Browse Source

Comply to eslint

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
John Molakvoæ (skjnldsv) 4 years ago
parent
commit
b9bc2417e7
100 changed files with 5160 additions and 3516 deletions
  1. 3 0
      Makefile
  2. 0 16
      apps/accessibility/.eslintrc.js
  3. 0 0
      apps/accessibility/js/accessibility.js
  4. 0 0
      apps/accessibility/js/accessibility.js.map
  5. 58 56
      apps/accessibility/src/Accessibility.vue
  6. 40 0
      apps/accessibility/src/components/ItemPreview.vue
  7. 0 28
      apps/accessibility/src/components/itemPreview.vue
  8. 6 6
      apps/accessibility/src/main.js
  9. 0 0
      apps/comments/js/comments.js
  10. 0 0
      apps/comments/js/comments.js.map
  11. 15 16
      apps/comments/src/activitytabviewplugin.js
  12. 2 3
      apps/comments/src/app.js
  13. 93 93
      apps/comments/src/commentcollection.js
  14. 52 52
      apps/comments/src/commentmodel.js
  15. 1 1
      apps/comments/src/comments.js
  16. 27 28
      apps/comments/src/commentsmodifymenu.js
  17. 509 509
      apps/comments/src/commentstabview.js
  18. 31 29
      apps/comments/src/commentsummarymodel.js
  19. 42 44
      apps/comments/src/filesplugin.js
  20. 51 50
      apps/comments/src/search.js
  21. 5 5
      apps/files/js/file-upload.js
  22. 1 1
      apps/files/js/files.js
  23. 1 1
      apps/files_external/js/statusmanager.js
  24. 0 22
      apps/files_sharing/.eslintrc.js
  25. 105 105
      apps/files_sharing/js/app.js
  26. 0 0
      apps/files_sharing/js/dist/additionalScripts.js
  27. 0 0
      apps/files_sharing/js/dist/additionalScripts.js.map
  28. 1 1
      apps/files_sharing/js/dist/collaboration.js
  29. 0 0
      apps/files_sharing/js/dist/collaboration.js.map
  30. 4 4
      apps/files_sharing/js/dist/files_sharing.4.js
  31. 0 0
      apps/files_sharing/js/dist/files_sharing.4.js.map
  32. 0 0
      apps/files_sharing/js/dist/files_sharing.js.map
  33. 333 337
      apps/files_sharing/js/sharedfilelist.js
  34. 6 4
      apps/files_sharing/src/additionalScripts.js
  35. 14 12
      apps/files_sharing/src/collaborationresources.js
  36. 12 10
      apps/files_sharing/src/collaborationresourceshandler.js
  37. 6 4
      apps/files_sharing/src/files_sharing.js
  38. 109 110
      apps/files_sharing/src/share.js
  39. 31 33
      apps/files_sharing/src/sharebreadcrumbview.js
  40. 64 65
      apps/files_sharing/src/sharetabview.js
  41. 11 8
      apps/files_sharing/src/views/CollaborationView.vue
  42. 0 0
      apps/files_trashbin/js/files_trashbin.js
  43. 0 0
      apps/files_trashbin/js/files_trashbin.js.map
  44. 53 54
      apps/files_trashbin/src/app.js
  45. 231 231
      apps/files_trashbin/src/filelist.js
  46. 0 0
      apps/files_versions/js/files_versions.js
  47. 0 0
      apps/files_versions/js/files_versions.js.map
  48. 6 7
      apps/files_versions/src/filesplugin.js
  49. 30 31
      apps/files_versions/src/versioncollection.js
  50. 28 28
      apps/files_versions/src/versionmodel.js
  51. 76 76
      apps/files_versions/src/versionstabview.js
  52. 0 0
      apps/oauth2/js/oauth2.js
  53. 0 0
      apps/oauth2/js/oauth2.js.map
  54. 50 31
      apps/oauth2/src/App.vue
  55. 18 16
      apps/oauth2/src/components/OAuthItem.vue
  56. 8 7
      apps/oauth2/src/main.js
  57. 0 0
      apps/settings/js/vue-0.js
  58. 0 0
      apps/settings/js/vue-0.js.map
  59. 0 0
      apps/settings/js/vue-4.js
  60. 0 0
      apps/settings/js/vue-4.js.map
  61. 0 0
      apps/settings/js/vue-5.js
  62. 0 0
      apps/settings/js/vue-5.js.map
  63. 1 1
      apps/settings/js/vue-6.js
  64. 0 0
      apps/settings/js/vue-6.js.map
  65. 0 0
      apps/settings/js/vue-settings-admin-security.js
  66. 0 0
      apps/settings/js/vue-settings-admin-security.js.map
  67. 0 0
      apps/settings/js/vue-settings-apps-users-management.js
  68. 0 0
      apps/settings/js/vue-settings-apps-users-management.js.map
  69. 0 0
      apps/settings/js/vue-settings-personal-security.js
  70. 0 0
      apps/settings/js/vue-settings-personal-security.js.map
  71. 3 3
      apps/settings/src/App.vue
  72. 103 106
      apps/settings/src/components/AdminTwoFactor.vue
  73. 326 0
      apps/settings/src/components/AppDetails.vue
  74. 201 0
      apps/settings/src/components/AppList.vue
  75. 179 0
      apps/settings/src/components/AppList/AppItem.vue
  76. 38 0
      apps/settings/src/components/AppList/AppScore.vue
  77. 138 0
      apps/settings/src/components/AppManagement.vue
  78. 64 64
      apps/settings/src/components/AuthToken.vue
  79. 48 48
      apps/settings/src/components/AuthTokenList.vue
  80. 138 134
      apps/settings/src/components/AuthTokenSection.vue
  81. 114 113
      apps/settings/src/components/AuthTokenSetupDialogue.vue
  82. 32 0
      apps/settings/src/components/PrefixMixin.vue
  83. 40 0
      apps/settings/src/components/SvgFilterMixin.vue
  84. 553 0
      apps/settings/src/components/UserList.vue
  85. 80 63
      apps/settings/src/components/appList.vue
  86. 12 12
      apps/settings/src/components/appList/appScore.vue
  87. 7 7
      apps/settings/src/components/prefixMixin.vue
  88. 16 16
      apps/settings/src/components/svgFilterMixin.vue
  89. 706 0
      apps/settings/src/components/userList/UserRow.vue
  90. 0 574
      apps/settings/src/components/userList/userRow.vue
  91. 4 3
      apps/settings/src/main-admin-security.js
  92. 15 14
      apps/settings/src/main-apps-users-management.js
  93. 12 11
      apps/settings/src/main-personal-security.js
  94. 6 6
      apps/settings/src/router.js
  95. 10 24
      apps/settings/src/store/admin-security.js
  96. 13 13
      apps/settings/src/store/api.js
  97. 130 130
      apps/settings/src/store/apps.js
  98. 15 15
      apps/settings/src/store/index.js
  99. 15 15
      apps/settings/src/store/oc.js
  100. 8 10
      apps/settings/src/store/settings.js

+ 3 - 0
Makefile

@@ -23,6 +23,9 @@ watch-js:
 lint-fix:
 	npm run lint:fix
 
+lint-fix-watch:
+	npm run lint:fix-watch
+
 # Cleaning
 clean:
 	rm -rf apps/accessibility/js/

+ 0 - 16
apps/accessibility/.eslintrc.js

@@ -1,16 +0,0 @@
-module.exports = {
-	env: {
-		browser: true,
-		es6: true
-	},
-	extends: 'eslint:recommended',
-	parserOptions: {
-		sourceType: 'module'
-	},
-	rules: {
-		indent: ['error', 'tab'],
-		'linebreak-style': ['error', 'unix'],
-		quotes: ['error', 'single'],
-		semi: ['error', 'always']
-	}
-};

File diff suppressed because it is too large
+ 0 - 0
apps/accessibility/js/accessibility.js


File diff suppressed because it is too large
+ 0 - 0
apps/accessibility/js/accessibility.js.map


+ 58 - 56
apps/accessibility/src/App.vue → apps/accessibility/src/Accessibility.vue

@@ -1,60 +1,56 @@
 <template>
 	<div id="accessibility" class="section">
-		<h2>{{t('accessibility', 'Accessibility')}}</h2>
+		<h2>{{ t('accessibility', 'Accessibility') }}</h2>
 		<p v-html="description" />
 		<p v-html="descriptionDetail" />
 
 		<div class="preview-list">
-			<preview :preview="highcontrast"
-				 :key="highcontrast.id" :selected="selected.highcontrast"
-				 v-on:select="selectHighContrast"></preview>
-			<preview v-for="preview in themes" :preview="preview"
-				 :key="preview.id" :selected="selected.theme"
-				 v-on:select="selectTheme"></preview>
-			<preview v-for="preview in fonts" :preview="preview"
-				 :key="preview.id" :selected="selected.font"
-				 v-on:select="selectFont"></preview>
+			<ItemPreview :key="highcontrast.id"
+				:preview="highcontrast"
+				:selected="selected.highcontrast"
+				@select="selectHighContrast" />
+			<ItemPreview v-for="preview in themes"
+				:key="preview.id"
+				:preview="preview"
+				:selected="selected.theme"
+				@select="selectTheme" />
+			<ItemPreview v-for="preview in fonts"
+				:key="preview.id"
+				:preview="preview"
+				:selected="selected.font"
+				@select="selectFont" />
 		</div>
 	</div>
 </template>
 
 <script>
-import preview from './components/itemPreview';
-import axios from 'nextcloud-axios';
+import ItemPreview from './components/ItemPreview'
+import axios from 'nextcloud-axios'
 
 export default {
 	name: 'Accessibility',
-	components: { preview },
-	beforeMount() {
-		// importing server data into the app
-		const serverDataElmt = document.getElementById('serverData');
-		if (serverDataElmt !== null) {
-			this.serverData = JSON.parse(
-				document.getElementById('serverData').dataset.server
-			);
-		}
-	},
+	components: { ItemPreview },
 	data() {
 		return {
 			serverData: []
-		};
+		}
 	},
 	computed: {
 		themes() {
-			return this.serverData.themes;
+			return this.serverData.themes
 		},
 		highcontrast() {
-			return this.serverData.highcontrast;
+			return this.serverData.highcontrast
 		},
 		fonts() {
-			return this.serverData.fonts;
+			return this.serverData.fonts
 		},
 		selected() {
 			return {
 				theme: this.serverData.selected.theme,
 				highcontrast: this.serverData.selected.highcontrast,
 				font: this.serverData.selected.font
-			};
+			}
 		},
 		description() {
 			// using the `t` replace method escape html, we have to do it manually :/
@@ -66,7 +62,7 @@ export default {
 				We aim to be compliant with the {guidelines} 2.1 on AA level,
 				with the high contrast theme even on AAA level.`
 			)
-			.replace('{guidelines}', this.guidelinesLink)
+				.replace('{guidelines}', this.guidelinesLink)
 		},
 		guidelinesLink() {
 			return `<a target="_blank" href="https://www.w3.org/WAI/standards-guidelines/wcag/" rel="noreferrer nofollow">${t('accessibility', 'Web Content Accessibility Guidelines')}</a>`
@@ -77,8 +73,8 @@ export default {
 				`If you find any issues, don’t hesitate to report them on {issuetracker}.
 				And if you want to get involved, come join {designteam}!`
 			)
-			.replace('{issuetracker}', this.issuetrackerLink)
-			.replace('{designteam}', this.designteamLink)
+				.replace('{issuetracker}', this.issuetrackerLink)
+				.replace('{designteam}', this.designteamLink)
 		},
 		issuetrackerLink() {
 			return `<a target="_blank" href="https://github.com/nextcloud/server/issues/" rel="noreferrer nofollow">${t('accessibility', 'our issue tracker')}</a>`
@@ -87,19 +83,28 @@ export default {
 			return `<a target="_blank" href="https://nextcloud.com/design" rel="noreferrer nofollow">${t('accessibility', 'our design team')}</a>`
 		}
 	},
+	beforeMount() {
+		// importing server data into the app
+		const serverDataElmt = document.getElementById('serverData')
+		if (serverDataElmt !== null) {
+			this.serverData = JSON.parse(
+				document.getElementById('serverData').dataset.server
+			)
+		}
+	},
 	methods: {
 		selectHighContrast(id) {
-			this.selectItem('highcontrast', id);
+			this.selectItem('highcontrast', id)
 		},
 		selectTheme(id, idSelectedBefore) {
-			this.selectItem('theme', id);
-			document.body.classList.remove(idSelectedBefore);
+			this.selectItem('theme', id)
+			document.body.classList.remove(idSelectedBefore)
 			if (id) {
-				document.body.classList.add(id);
+				document.body.classList.add(id)
 			}
 		},
 		selectFont(id) {
-			this.selectItem('font', id);
+			this.selectItem('font', id)
 		},
 
 		/**
@@ -110,43 +115,40 @@ export default {
 		 * @param {string} id the data of the change
 		 */
 		selectItem(type, id) {
-			axios.post(
-					OC.linkToOCS('apps/accessibility/api/v1/config', 2) + type,
-					{ value: id }
-				)
+			axios.post(OC.linkToOCS('apps/accessibility/api/v1/config', 2) + type, { value: id })
 				.then(response => {
-					this.serverData.selected[type] = id;
+					this.serverData.selected[type] = id
 
 					// Remove old link
-					let link = document.querySelector('link[rel=stylesheet][href*=accessibility][href*=user-]');
+					let link = document.querySelector('link[rel=stylesheet][href*=accessibility][href*=user-]')
 					if (!link) {
 						// insert new css
-						let link = document.createElement('link');
-						link.rel = 'stylesheet';
-						link.href = OC.generateUrl('/apps/accessibility/css/user-style.css') + '?v=' + new Date().getTime();
-						document.head.appendChild(link);
+						let link = document.createElement('link')
+						link.rel = 'stylesheet'
+						link.href = OC.generateUrl('/apps/accessibility/css/user-style.css') + '?v=' + new Date().getTime()
+						document.head.appendChild(link)
 					} else {
 						// compare arrays
 						if (
-							JSON.stringify(Object.values(this.selected)) ===
-							JSON.stringify([false, false])
+							JSON.stringify(Object.values(this.selected))
+							=== JSON.stringify([false, false])
 						) {
 							// if nothing is selected, blindly remove the css
-							link.remove();
+							link.remove()
 						} else {
 							// force update
-							link.href =
-								link.href.split('?')[0] +
-								'?v=' +
-								new Date().getTime();
+							link.href
+								= link.href.split('?')[0]
+								+ '?v='
+								+ new Date().getTime()
 						}
 					}
 				})
 				.catch(err => {
-					console.log(err, err.response);
-					OC.Notification.showTemporary(t('accessibility', err.response.data.ocs.meta.message + '. Unable to apply the setting.'));
-				});
+					console.error(err, err.response)
+					OC.Notification.showTemporary(t('accessibility', err.response.data.ocs.meta.message + '. Unable to apply the setting.'))
+				})
 		}
 	}
-};
+}
 </script>

+ 40 - 0
apps/accessibility/src/components/ItemPreview.vue

@@ -0,0 +1,40 @@
+<template>
+	<div :class="{preview: true}">
+		<div class="preview-image" :style="{backgroundImage: 'url(' + preview.img + ')'}" />
+		<div class="preview-description">
+			<h3>{{ preview.title }}</h3>
+			<p>{{ preview.text }}</p>
+			<input :id="'accessibility-' + preview.id"
+				v-model="checked"
+				type="checkbox"
+				class="checkbox">
+			<label :for="'accessibility-' + preview.id">{{ t('accessibility', 'Enable') }} {{ preview.title.toLowerCase() }}</label>
+		</div>
+	</div>
+</template>
+
+<script>
+export default {
+	name: 'ItemPreview',
+	props: {
+		preview: {
+			type: Object,
+			required: true
+		},
+		selected: {
+			type: String,
+			default: null
+		}
+	},
+	computed: {
+		checked: {
+			get() {
+				return this.selected === this.preview.id
+			},
+			set(checked) {
+				this.$emit('select', checked ? this.preview.id : false,	this.selected)
+			}
+		}
+	}
+}
+</script>

+ 0 - 28
apps/accessibility/src/components/itemPreview.vue

@@ -1,28 +0,0 @@
-<template>
-	<div :class="{preview: true}">
-		<div class="preview-image" :style="{backgroundImage: 'url(' + preview.img + ')'}"></div>
-		<div class="preview-description">
-			<h3>{{preview.title}}</h3>
-			<p>{{preview.text}}</p>
-			<input type="checkbox" class="checkbox" :id="'accessibility-' + preview.id" v-model="checked" />
-			<label :for="'accessibility-' + preview.id">{{t('accessibility', 'Enable')}} {{preview.title.toLowerCase()}}</label>
-		</div>
-	</div>
-</template>
-
-<script>
-export default {
-	name: 'itemPreview',
-	props: ['preview', 'selected'],
-	computed: {
-		checked: {
-			get() {
-				return this.selected === this.preview.id;
-			},
-			set(checked) {
-				this.$emit('select', checked ? this.preview.id : false,	this.selected);
-			}
-		}
-    },
-};
-</script>

+ 6 - 6
apps/accessibility/src/main.js

@@ -1,12 +1,12 @@
-import Vue from 'vue';
-import App from './App.vue';
+import Vue from 'vue'
+import App from './Accessibility.vue'
 
 /* global t */
 // bind to window
-Vue.prototype.OC = OC;
-Vue.prototype.t = t;
+Vue.prototype.OC = OC
+Vue.prototype.t = t
 
-new Vue({
+export default new Vue({
 	el: '#accessibility',
 	render: h => h(App)
-});
+})

File diff suppressed because it is too large
+ 0 - 0
apps/comments/js/comments.js


File diff suppressed because it is too large
+ 0 - 0
apps/comments/js/comments.js.map


+ 15 - 16
apps/comments/src/activitytabviewplugin.js

@@ -1,4 +1,4 @@
-/*
+/**
  * @author Joas Schilling <coding@schilljs.com>
  * Copyright (c) 2016
  *
@@ -18,18 +18,18 @@
 		 * @param {jQuery} $el jQuery handle for this activity
 		 * @param {string} view The view that displayes this activity
 		 */
-		prepareModelForDisplay: function (model, $el, view) {
+		prepareModelForDisplay: function(model, $el, view) {
 			if (model.get('app') !== 'comments' || model.get('type') !== 'comments') {
-				return;
+				return
 			}
 
 			if (view === 'ActivityTabView') {
-				$el.addClass('comment');
+				$el.addClass('comment')
 				if (model.get('message') && this._isLong(model.get('message'))) {
-					$el.addClass('collapsed');
-					var $overlay = $('<div>').addClass('message-overlay');
-					$el.find('.activitymessage').after($overlay);
-					$el.on('click', this._onClickCollapsedComment);
+					$el.addClass('collapsed')
+					var $overlay = $('<div>').addClass('message-overlay')
+					$el.find('.activitymessage').after($overlay)
+					$el.on('click', this._onClickCollapsedComment)
 				}
 			}
 		},
@@ -38,22 +38,21 @@
 		 * Copy of CommentsTabView._onClickComment()
 		 */
 		_onClickCollapsedComment: function(ev) {
-			var $row = $(ev.target);
+			var $row = $(ev.target)
 			if (!$row.is('.comment')) {
-				$row = $row.closest('.comment');
+				$row = $row.closest('.comment')
 			}
-			$row.removeClass('collapsed');
+			$row.removeClass('collapsed')
 		},
 
 		/*
 		 * Copy of CommentsTabView._isLong()
 		 */
 		_isLong: function(message) {
-			return message.length > 250 || (message.match(/\n/g) || []).length > 1;
+			return message.length > 250 || (message.match(/\n/g) || []).length > 1
 		}
-	};
+	}
 
+})()
 
-})();
-
-OC.Plugins.register('OCA.Activity.RenderingPlugins', OCA.Comments.ActivityTabViewPlugin);
+OC.Plugins.register('OCA.Activity.RenderingPlugins', OCA.Comments.ActivityTabViewPlugin)

+ 2 - 3
apps/comments/src/app.js

@@ -13,8 +13,7 @@
 		/**
 		 * @namespace
 		 */
-		OCA.Comments = {};
+		OCA.Comments = {}
 	}
 
-})();
-
+})()

+ 93 - 93
apps/comments/src/commentcollection.js

@@ -1,3 +1,4 @@
+/* eslint-disable */
 /*
  * Copyright (c) 2016
  *
@@ -20,148 +21,147 @@
 	var CommentCollection = OC.Backbone.Collection.extend(
 		/** @lends OCA.Comments.CommentCollection.prototype */ {
 
-		sync: OC.Backbone.davSync,
+			sync: OC.Backbone.davSync,
 
-		model: OCA.Comments.CommentModel,
+			model: OCA.Comments.CommentModel,
 
-		/**
+			/**
 		 * Object type
 		 *
 		 * @type string
 		 */
-		_objectType: 'files',
+			_objectType: 'files',
 
-		/**
+			/**
 		 * Object id
 		 *
 		 * @type string
 		 */
-		_objectId: null,
+			_objectId: null,
 
-		/**
+			/**
 		 * True if there are no more page results left to fetch
 		 *
 		 * @type bool
 		 */
-		_endReached: false,
+			_endReached: false,
 
-		/**
+			/**
 		 * Number of comments to fetch per page
 		 *
 		 * @type int
 		 */
-		_limit : 20,
+			_limit: 20,
 
-		/**
+			/**
 		 * Initializes the collection
 		 *
 		 * @param {string} [options.objectType] object type
 		 * @param {string} [options.objectId] object id
 		 */
-		initialize: function(models, options) {
-			options = options || {};
-			if (options.objectType) {
-				this._objectType = options.objectType;
-			}
-			if (options.objectId) {
-				this._objectId = options.objectId;
-			}
-		},
+			initialize: function(models, options) {
+				options = options || {}
+				if (options.objectType) {
+					this._objectType = options.objectType
+				}
+				if (options.objectId) {
+					this._objectId = options.objectId
+				}
+			},
 
-		url: function() {
-			return OC.linkToRemote('dav') + '/comments/' +
-				encodeURIComponent(this._objectType) + '/' +
-				encodeURIComponent(this._objectId) + '/';
-		},
+			url: function() {
+				return OC.linkToRemote('dav') + '/comments/'
+				+ encodeURIComponent(this._objectType) + '/'
+				+ encodeURIComponent(this._objectId) + '/'
+			},
 
-		setObjectId: function(objectId) {
-			this._objectId = objectId;
-		},
+			setObjectId: function(objectId) {
+				this._objectId = objectId
+			},
 
-		hasMoreResults: function() {
-			return !this._endReached;
-		},
+			hasMoreResults: function() {
+				return !this._endReached
+			},
 
-		reset: function() {
-			this._endReached = false;
-			this._summaryModel = null;
-			return OC.Backbone.Collection.prototype.reset.apply(this, arguments);
-		},
+			reset: function() {
+				this._endReached = false
+				this._summaryModel = null
+				return OC.Backbone.Collection.prototype.reset.apply(this, arguments)
+			},
 
-		/**
+			/**
 		 * Fetch the next set of results
 		 */
-		fetchNext: function(options) {
-			var self = this;
-			if (!this.hasMoreResults()) {
-				return null;
-			}
+			fetchNext: function(options) {
+				var self = this
+				if (!this.hasMoreResults()) {
+					return null
+				}
 
-			var body = '<?xml version="1.0" encoding="utf-8" ?>\n' +
-				'<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">\n' +
+				var body = '<?xml version="1.0" encoding="utf-8" ?>\n'
+				+ '<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">\n'
 				// load one more so we know there is more
-				'    <oc:limit>' + (this._limit + 1) + '</oc:limit>\n' +
-				'    <oc:offset>' + this.length + '</oc:offset>\n' +
-				'</oc:filter-comments>\n';
-
-			options = options || {};
-			var success = options.success;
-			options = _.extend({
-				remove: false,
-				parse: true,
-				data: body,
-				davProperties: CommentCollection.prototype.model.prototype.davProperties,
-				success: function(resp) {
-					if (resp.length <= self._limit) {
+				+ '    <oc:limit>' + (this._limit + 1) + '</oc:limit>\n'
+				+ '    <oc:offset>' + this.length + '</oc:offset>\n'
+				+ '</oc:filter-comments>\n'
+
+				options = options || {}
+				var success = options.success
+				options = _.extend({
+					remove: false,
+					parse: true,
+					data: body,
+					davProperties: CommentCollection.prototype.model.prototype.davProperties,
+					success: function(resp) {
+						if (resp.length <= self._limit) {
 						// no new entries, end reached
-						self._endReached = true;
-					} else {
+							self._endReached = true
+						} else {
 						// remove last entry, for next page load
-						resp = _.initial(resp);
-					}
-					if (!self.set(resp, options)) {
-						return false;
+							resp = _.initial(resp)
+						}
+						if (!self.set(resp, options)) {
+							return false
+						}
+						if (success) {
+							success.apply(null, arguments)
+						}
+						self.trigger('sync', 'REPORT', self, options)
 					}
-					if (success) {
-						success.apply(null, arguments);
-					}
-					self.trigger('sync', 'REPORT', self, options);
-				}
-			}, options);
+				}, options)
 
-			return this.sync('REPORT', this, options);
-		},
+				return this.sync('REPORT', this, options)
+			},
 
-		/**
+			/**
 		 * Returns the matching summary model
 		 *
-		 * @return {OCA.Comments.CommentSummaryModel} summary model
+		 * @returns {OCA.Comments.CommentSummaryModel} summary model
 		 */
-		getSummaryModel: function() {
-			if (!this._summaryModel) {
-				this._summaryModel = new OCA.Comments.CommentSummaryModel({
-					id: this._objectId,
-					objectType: this._objectType
-				});
-			}
-			return this._summaryModel;
-		},
+			getSummaryModel: function() {
+				if (!this._summaryModel) {
+					this._summaryModel = new OCA.Comments.CommentSummaryModel({
+						id: this._objectId,
+						objectType: this._objectType
+					})
+				}
+				return this._summaryModel
+			},
 
-		/**
+			/**
 		 * Updates the read marker for this comment thread
 		 *
 		 * @param {Date} [date] optional date, defaults to now
 		 * @param {Object} [options] backbone options
 		 */
-		updateReadMarker: function(date, options) {
-			options = options || {};
-
-			return this.getSummaryModel().save({
-				readMarker: (date || new Date()).toUTCString()
-			}, options);
-		}
-	});
+			updateReadMarker: function(date, options) {
+				options = options || {}
 
-	OCA.Comments.CommentCollection = CommentCollection;
-})(OC, OCA);
+				return this.getSummaryModel().save({
+					readMarker: (date || new Date()).toUTCString()
+				}, options)
+			}
+		})
 
+	OCA.Comments.CommentCollection = CommentCollection
+})(OC, OCA)

+ 52 - 52
apps/comments/src/commentmodel.js

@@ -12,7 +12,7 @@
 
 	_.extend(OC.Files.Client, {
 		PROPERTY_FILEID:	'{' + OC.Files.Client.NS_OWNCLOUD + '}id',
-		PROPERTY_MESSAGE: 	'{' + OC.Files.Client.NS_OWNCLOUD + '}message',
+		PROPERTY_MESSAGE: '{' + OC.Files.Client.NS_OWNCLOUD + '}message',
 		PROPERTY_ACTORTYPE:	'{' + OC.Files.Client.NS_OWNCLOUD + '}actorType',
 		PROPERTY_ACTORID:	'{' + OC.Files.Client.NS_OWNCLOUD + '}actorId',
 		PROPERTY_ISUNREAD:	'{' + OC.Files.Client.NS_OWNCLOUD + '}isUnread',
@@ -21,7 +21,7 @@
 		PROPERTY_ACTORDISPLAYNAME:	'{' + OC.Files.Client.NS_OWNCLOUD + '}actorDisplayName',
 		PROPERTY_CREATIONDATETIME:	'{' + OC.Files.Client.NS_OWNCLOUD + '}creationDateTime',
 		PROPERTY_MENTIONS: '{' + OC.Files.Client.NS_OWNCLOUD + '}mentions'
-	});
+	})
 
 	/**
 	 * @class OCA.Comments.CommentModel
@@ -32,62 +32,62 @@
 	 */
 	var CommentModel = OC.Backbone.Model.extend(
 		/** @lends OCA.Comments.CommentModel.prototype */ {
-		sync: OC.Backbone.davSync,
+			sync: OC.Backbone.davSync,
 
-		defaults: {
-			actorType: 'users',
-			objectType: 'files'
-		},
+			defaults: {
+				actorType: 'users',
+				objectType: 'files'
+			},
 
-		davProperties: {
-			'id':	OC.Files.Client.PROPERTY_FILEID,
-			'message':	OC.Files.Client.PROPERTY_MESSAGE,
-			'actorType':	OC.Files.Client.PROPERTY_ACTORTYPE,
-			'actorId':	OC.Files.Client.PROPERTY_ACTORID,
-			'actorDisplayName':	OC.Files.Client.PROPERTY_ACTORDISPLAYNAME,
-			'creationDateTime':	OC.Files.Client.PROPERTY_CREATIONDATETIME,
-			'objectType':	OC.Files.Client.PROPERTY_OBJECTTYPE,
-			'objectId':	OC.Files.Client.PROPERTY_OBJECTID,
-			'isUnread':	OC.Files.Client.PROPERTY_ISUNREAD,
-			'mentions': OC.Files.Client.PROPERTY_MENTIONS
-		},
+			davProperties: {
+				'id':	OC.Files.Client.PROPERTY_FILEID,
+				'message':	OC.Files.Client.PROPERTY_MESSAGE,
+				'actorType':	OC.Files.Client.PROPERTY_ACTORTYPE,
+				'actorId':	OC.Files.Client.PROPERTY_ACTORID,
+				'actorDisplayName':	OC.Files.Client.PROPERTY_ACTORDISPLAYNAME,
+				'creationDateTime':	OC.Files.Client.PROPERTY_CREATIONDATETIME,
+				'objectType':	OC.Files.Client.PROPERTY_OBJECTTYPE,
+				'objectId':	OC.Files.Client.PROPERTY_OBJECTID,
+				'isUnread':	OC.Files.Client.PROPERTY_ISUNREAD,
+				'mentions': OC.Files.Client.PROPERTY_MENTIONS
+			},
 
-		parse: function(data) {
-			return {
-				id: data.id,
-				message: data.message,
-				actorType: data.actorType,
-				actorId: data.actorId,
-				actorDisplayName: data.actorDisplayName,
-				creationDateTime: data.creationDateTime,
-				objectType: data.objectType,
-				objectId: data.objectId,
-				isUnread: (data.isUnread === 'true'),
-				mentions: this._parseMentions(data.mentions)
-			};
-		},
+			parse: function(data) {
+				return {
+					id: data.id,
+					message: data.message,
+					actorType: data.actorType,
+					actorId: data.actorId,
+					actorDisplayName: data.actorDisplayName,
+					creationDateTime: data.creationDateTime,
+					objectType: data.objectType,
+					objectId: data.objectId,
+					isUnread: (data.isUnread === 'true'),
+					mentions: this._parseMentions(data.mentions)
+				}
+			},
 
-		_parseMentions: function(mentions) {
-			if(_.isUndefined(mentions)) {
-				return {};
-			}
-			var result = {};
-			for(var i in mentions) {
-				var mention = mentions[i];
-				if(_.isUndefined(mention.localName) || mention.localName !== 'mention') {
-					continue;
+			_parseMentions: function(mentions) {
+				if (_.isUndefined(mentions)) {
+					return {}
 				}
-				result[i] = {};
-				for (var child = mention.firstChild; child; child = child.nextSibling) {
-					if(_.isUndefined(child.localName) || !child.localName.startsWith('mention')) {
-						continue;
+				var result = {}
+				for (var i in mentions) {
+					var mention = mentions[i]
+					if (_.isUndefined(mention.localName) || mention.localName !== 'mention') {
+						continue
+					}
+					result[i] = {}
+					for (var child = mention.firstChild; child; child = child.nextSibling) {
+						if (_.isUndefined(child.localName) || !child.localName.startsWith('mention')) {
+							continue
+						}
+						result[i][child.localName] = child.textContent
 					}
-					result[i][child.localName] = child.textContent;
 				}
+				return result
 			}
-			return result;
-		}
-	});
+		})
 
-	OCA.Comments.CommentModel = CommentModel;
-})(OC, OCA);
+	OCA.Comments.CommentModel = CommentModel
+})(OC, OCA)

+ 1 - 1
apps/comments/src/comments.js

@@ -15,4 +15,4 @@ import './vendor/At.js/dist/js/jquery.atwho.min'
 import './style/autocomplete.scss'
 import './style/comments.scss'
 
-window.OCA.Comments = OCA.Comments;
+window.OCA.Comments = OCA.Comments

+ 27 - 28
apps/comments/src/commentsmodifymenu.js

@@ -8,7 +8,6 @@
  *
  */
 
-/* global Handlebars */
 (function() {
 
 	/**
@@ -23,7 +22,7 @@
 		_scopes: [
 			{
 				name: 'edit',
-				displayName:  t('comments', 'Edit comment'),
+				displayName: t('comments', 'Edit comment'),
 				iconClass: 'icon-rename'
 			},
 			{
@@ -45,14 +44,14 @@
 		 * @param {Object} event event object
 		 */
 		_onClickAction: function(event) {
-			var $target = $(event.currentTarget);
+			var $target = $(event.currentTarget)
 			if (!$target.hasClass('menuitem')) {
-				$target = $target.closest('.menuitem');
+				$target = $target.closest('.menuitem')
 			}
 
-			OC.hideMenus();
+			OC.hideMenus()
 
-			this.trigger('select:menu-item-clicked', event, $target.data('action'));
+			this.trigger('select:menu-item-clicked', event, $target.data('action'))
 		},
 
 		/**
@@ -61,49 +60,49 @@
 		render: function() {
 			this.$el.html(OCA.Comments.Templates['commentsmodifymenu']({
 				items: this._scopes
-			}));
+			}))
 		},
 
 		/**
 		 * Displays the menu
+		 * @param {Event} context the click event
 		 */
 		show: function(context) {
-			this._context = context;
+			this._context = context
 
-			for(var i in this._scopes) {
-				this._scopes[i].active = false;
+			for (var i in this._scopes) {
+				this._scopes[i].active = false
 			}
 
-
-			var $el = $(context.target);
-			var offsetIcon = $el.offset();
-			var offsetContainer = $el.closest('.authorRow').offset();
+			var $el = $(context.target)
+			var offsetIcon = $el.offset()
+			var offsetContainer = $el.closest('.authorRow').offset()
 
 			// adding some extra top offset to push the menu below the button.
 			var position = {
 				top: offsetIcon.top - offsetContainer.top + 48,
 				left: '',
 				right: ''
-			};
+			}
 
-			position.left = offsetIcon.left - offsetContainer.left;
+			position.left = offsetIcon.left - offsetContainer.left
 
 			if (position.left > 200) {
 				// we need to position the menu to the right.
-				position.left = '';
-				position.right = this.$el.closest('.comment').find('.date').width();
-				this.$el.removeClass('menu-left').addClass('menu-right');
+				position.left = ''
+				position.right = this.$el.closest('.comment').find('.date').width()
+				this.$el.removeClass('menu-left').addClass('menu-right')
 			} else {
-				this.$el.removeClass('menu-right').addClass('menu-left');
+				this.$el.removeClass('menu-right').addClass('menu-left')
 			}
-			this.$el.css(position);
-			this.render();
-			this.$el.removeClass('hidden');
+			this.$el.css(position)
+			this.render()
+			this.$el.removeClass('hidden')
 
-			OC.showMenu(null, this.$el);
+			OC.showMenu(null, this.$el)
 		}
-	});
+	})
 
-	OCA.Comments = OCA.Comments || {};
-	OCA.Comments.CommentsModifyMenu = CommentsModifyMenu;
-})(OC, OCA);
+	OCA.Comments = OCA.Comments || {}
+	OCA.Comments.CommentsModifyMenu = CommentsModifyMenu
+})(OC, OCA)

File diff suppressed because it is too large
+ 509 - 509
apps/comments/src/commentstabview.js


+ 31 - 29
apps/comments/src/commentsummarymodel.js

@@ -12,7 +12,7 @@
 
 	_.extend(OC.Files.Client, {
 		PROPERTY_READMARKER:	'{' + OC.Files.Client.NS_OWNCLOUD + '}readMarker'
-	});
+	})
 
 	/**
 	 * @class OCA.Comments.CommentSummaryModel
@@ -24,45 +24,47 @@
 	 */
 	var CommentSummaryModel = OC.Backbone.Model.extend(
 		/** @lends OCA.Comments.CommentSummaryModel.prototype */ {
-		sync: OC.Backbone.davSync,
+			sync: OC.Backbone.davSync,
 
-		/**
+			/**
 		 * Object type
 		 *
 		 * @type string
 		 */
-		_objectType: 'files',
+			_objectType: 'files',
 
-		/**
+			/**
 		 * Object id
 		 *
 		 * @type string
 		 */
-		_objectId: null,
+			_objectId: null,
 
-		davProperties: {
-			'readMarker': OC.Files.Client.PROPERTY_READMARKER
-		},
+			davProperties: {
+				'readMarker': OC.Files.Client.PROPERTY_READMARKER
+			},
 
-		/**
-		 * Initializes the summary model
-		 *
-		 * @param {string} [options.objectType] object type
-		 * @param {string} [options.objectId] object id
-		 */
-		initialize: function(attrs, options) {
-			options = options || {};
-			if (options.objectType) {
-				this._objectType = options.objectType;
-			}
-		},
+			/**
+			 * Initializes the summary model
+			 *
+			 * @param {any} [attrs] ignored
+			 * @param {Object} [options] destructuring object
+			 * @param {string} [options.objectType] object type
+			 * @param {string} [options.objectId] object id
+			 */
+			initialize: function(attrs, options) {
+				options = options || {}
+				if (options.objectType) {
+					this._objectType = options.objectType
+				}
+			},
 
-		url: function() {
-			return OC.linkToRemote('dav') + '/comments/' +
-				encodeURIComponent(this._objectType) + '/' +
-				encodeURIComponent(this.id) + '/';
-		}
-	});
+			url: function() {
+				return OC.linkToRemote('dav') + '/comments/'
+				+ encodeURIComponent(this._objectType) + '/'
+				+ encodeURIComponent(this.id) + '/'
+			}
+		})
 
-	OCA.Comments.CommentSummaryModel = CommentSummaryModel;
-})(OC, OCA);
+	OCA.Comments.CommentSummaryModel = CommentSummaryModel
+})(OC, OCA)

+ 42 - 44
apps/comments/src/filesplugin.js

@@ -8,20 +8,18 @@
  *
  */
 
-/* global Handlebars */
-
 (function() {
 
 	_.extend(OC.Files.Client, {
 		PROPERTY_COMMENTS_UNREAD:	'{' + OC.Files.Client.NS_OWNCLOUD + '}comments-unread'
-	});
+	})
 
-	OCA.Comments = _.extend({}, OCA.Comments);
+	OCA.Comments = _.extend({}, OCA.Comments)
 	if (!OCA.Comments) {
 		/**
 		 * @namespace
 		 */
-		OCA.Comments = {};
+		OCA.Comments = {}
 	}
 
 	/**
@@ -38,43 +36,43 @@
 				count: count,
 				countMessage: n('comments', '%n unread comment', '%n unread comments', count),
 				iconUrl: OC.imagePath('core', 'actions/comment')
-			});
+			})
 		},
 
 		attach: function(fileList) {
-			var self = this;
+			var self = this
 			if (this.ignoreLists.indexOf(fileList.id) >= 0) {
-				return;
+				return
 			}
 
-			fileList.registerTabView(new OCA.Comments.CommentsTabView('commentsTabView'));
+			fileList.registerTabView(new OCA.Comments.CommentsTabView('commentsTabView'))
 
-			var oldGetWebdavProperties = fileList._getWebdavProperties;
+			var oldGetWebdavProperties = fileList._getWebdavProperties
 			fileList._getWebdavProperties = function() {
-				var props = oldGetWebdavProperties.apply(this, arguments);
-				props.push(OC.Files.Client.PROPERTY_COMMENTS_UNREAD);
-				return props;
-			};
+				var props = oldGetWebdavProperties.apply(this, arguments)
+				props.push(OC.Files.Client.PROPERTY_COMMENTS_UNREAD)
+				return props
+			}
 
 			fileList.filesClient.addFileInfoParser(function(response) {
-				var data = {};
-				var props = response.propStat[0].properties;
-				var commentsUnread = props[OC.Files.Client.PROPERTY_COMMENTS_UNREAD];
+				var data = {}
+				var props = response.propStat[0].properties
+				var commentsUnread = props[OC.Files.Client.PROPERTY_COMMENTS_UNREAD]
 				if (!_.isUndefined(commentsUnread) && commentsUnread !== '') {
-					data.commentsUnread = parseInt(commentsUnread, 10);
+					data.commentsUnread = parseInt(commentsUnread, 10)
 				}
-				return data;
-			});
+				return data
+			})
 
-			fileList.$el.addClass('has-comments');
-			var oldCreateRow = fileList._createRow;
+			fileList.$el.addClass('has-comments')
+			var oldCreateRow = fileList._createRow
 			fileList._createRow = function(fileData) {
-				var $tr = oldCreateRow.apply(this, arguments);
+				var $tr = oldCreateRow.apply(this, arguments)
 				if (fileData.commentsUnread) {
-					$tr.attr('data-comments-unread', fileData.commentsUnread);
+					$tr.attr('data-comments-unread', fileData.commentsUnread)
 				}
-				return $tr;
-			};
+				return $tr
+			}
 
 			// register "comment" action for reading comments
 			fileList.fileActions.registerAction({
@@ -94,35 +92,35 @@
 				permissions: OC.PERMISSION_READ,
 				type: OCA.Files.FileActions.TYPE_INLINE,
 				render: function(actionSpec, isDefault, context) {
-					var $file = context.$file;
-					var unreadComments = $file.data('comments-unread');
+					var $file = context.$file
+					var unreadComments = $file.data('comments-unread')
 					if (unreadComments) {
-						var $actionLink = $(self._formatCommentCount(unreadComments));
-						context.$file.find('a.name>span.fileactions').append($actionLink);
-						return $actionLink;
+						var $actionLink = $(self._formatCommentCount(unreadComments))
+						context.$file.find('a.name>span.fileactions').append($actionLink)
+						return $actionLink
 					}
-					return '';
+					return ''
 				},
 				actionHandler: function(fileName, context) {
-					context.$file.find('.action-comment').tooltip('hide');
+					context.$file.find('.action-comment').tooltip('hide')
 					// open sidebar in comments section
-					context.fileList.showDetailsView(fileName, 'commentsTabView');
+					context.fileList.showDetailsView(fileName, 'commentsTabView')
 				}
-			});
+			})
 
 			// add attribute to "elementToFile"
-			var oldElementToFile = fileList.elementToFile;
+			var oldElementToFile = fileList.elementToFile
 			fileList.elementToFile = function($el) {
-				var fileInfo = oldElementToFile.apply(this, arguments);
-				var commentsUnread = $el.data('comments-unread');
+				var fileInfo = oldElementToFile.apply(this, arguments)
+				var commentsUnread = $el.data('comments-unread')
 				if (commentsUnread) {
-					fileInfo.commentsUnread = commentsUnread;
+					fileInfo.commentsUnread = commentsUnread
 				}
-				return fileInfo;
-			};
+				return fileInfo
+			}
 		}
-	};
+	}
 
-})();
+})()
 
-OC.Plugins.register('OCA.Files.FileList', OCA.Comments.FilesPlugin);
+OC.Plugins.register('OCA.Files.FileList', OCA.Comments.FilesPlugin)

+ 51 - 50
apps/comments/src/search.js

@@ -1,3 +1,4 @@
+/* eslint-disable */
 /*
  * Copyright (c) 2014
  *
@@ -8,15 +9,15 @@
  *
  */
 (function(OC, OCA, $) {
-	"use strict";
+	'use strict'
 
 	/**
 	 * Construct a new FileActions instance
 	 * @constructs Files
 	 */
 	var Comment = function() {
-		this.initialize();
-	};
+		this.initialize()
+	}
 
 	Comment.prototype = {
 
@@ -27,25 +28,25 @@
 		 */
 		initialize: function() {
 
-			var self = this;
+			var self = this
 
 			this.fileAppLoaded = function() {
-				return !!OCA.Files && !!OCA.Files.App;
-			};
+				return !!OCA.Files && !!OCA.Files.App
+			}
 			function inFileList($row, result) {
-				return false;
+				return false
 
-				if (! self.fileAppLoaded()) {
-					return false;
+				if (!self.fileAppLoaded()) {
+					return false
 				}
-				var dir = self.fileList.getCurrentDirectory().replace(/\/+$/,'');
-				var resultDir = OC.dirname(result.path);
-				return dir === resultDir && self.fileList.inList(result.name);
+				var dir = self.fileList.getCurrentDirectory().replace(/\/+$/, '')
+				var resultDir = OC.dirname(result.path)
+				return dir === resultDir && self.fileList.inList(result.name)
 			}
 			function hideNoFilterResults() {
-				var $nofilterresults = $('.nofilterresults');
-				if ( ! $nofilterresults.hasClass('hidden') ) {
-					$nofilterresults.addClass('hidden');
+				var $nofilterresults = $('.nofilterresults')
+				if (!$nofilterresults.hasClass('hidden')) {
+					$nofilterresults.addClass('hidden')
 				}
 			}
 
@@ -64,73 +65,73 @@
 			 */
 			this.renderCommentResult = function($row, result) {
 				if (inFileList($row, result)) {
-					return null;
+					return null
 				}
-				hideNoFilterResults();
-				/*render preview icon, show path beneath filename,
+				hideNoFilterResults()
+				/* render preview icon, show path beneath filename,
 				 show size and last modified date on the right */
-				this.updateLegacyMimetype(result);
+				this.updateLegacyMimetype(result)
 
-				var $pathDiv = $('<div>').addClass('path').text(result.path);
+				var $pathDiv = $('<div>').addClass('path').text(result.path)
 
-				var $avatar = $('<div>');
+				var $avatar = $('<div>')
 				$avatar.addClass('avatar')
 					.css('display', 'inline-block')
 					.css('vertical-align', 'middle')
-					.css('margin', '0 5px 2px 3px');
+					.css('margin', '0 5px 2px 3px')
 
 				if (result.authorName) {
-					$avatar.avatar(result.authorId, 21, undefined, false, undefined, result.authorName);
+					$avatar.avatar(result.authorId, 21, undefined, false, undefined, result.authorName)
 				} else {
-					$avatar.avatar(result.authorId, 21);
+					$avatar.avatar(result.authorId, 21)
 				}
 
-				$row.find('td.info div.name').after($pathDiv).text(result.comment).prepend($('<span>').addClass('path').css('margin-right', '5px').text(result.authorName)).prepend($avatar);
-				$row.find('td.result a').attr('href', result.link);
+				$row.find('td.info div.name').after($pathDiv).text(result.comment).prepend($('<span>').addClass('path').css('margin-right', '5px').text(result.authorName)).prepend($avatar)
+				$row.find('td.result a').attr('href', result.link)
 
 				$row.find('td.icon')
 					.css('background-image', 'url(' + OC.imagePath('core', 'actions/comment') + ')')
-					.css('opacity', '.4');
-				var dir = OC.dirname(result.path);
+					.css('opacity', '.4')
+				var dir = OC.dirname(result.path)
 				// "result.path" does not include a leading "/", so "OC.dirname"
 				// returns the path itself for files or folders in the root.
 				if (dir === result.path) {
-					dir = '/';
+					dir = '/'
 				}
 				$row.find('td.info a').attr('href',
-					OC.generateUrl('/apps/files/?dir={dir}&scrollto={scrollto}', {dir: dir, scrollto: result.fileName})
-				);
+					OC.generateUrl('/apps/files/?dir={dir}&scrollto={scrollto}', { dir: dir, scrollto: result.fileName })
+				)
 
-				return $row;
-			};
+				return $row
+			}
 
 			this.handleCommentClick = function($row, result, event) {
 				if (self.fileAppLoaded() && self.fileList.id === 'files') {
-					self.fileList.changeDirectory(OC.dirname(result.path));
-					self.fileList.scrollTo(result.name);
-					return false;
+					self.fileList.changeDirectory(OC.dirname(result.path))
+					self.fileList.scrollTo(result.name)
+					return false
 				} else {
-					return true;
+					return true
 				}
-			};
+			}
 
-			this.updateLegacyMimetype = function (result) {
+			this.updateLegacyMimetype = function(result) {
 				// backward compatibility:
 				if (!result.mime && result.mime_type) {
-					result.mime = result.mime_type;
+					result.mime = result.mime_type
 				}
-			};
-			this.setFileList = function (fileList) {
-				this.fileList = fileList;
-			};
+			}
+			this.setFileList = function(fileList) {
+				this.fileList = fileList
+			}
 
-			OC.Plugins.register('OCA.Search.Core', this);
+			OC.Plugins.register('OCA.Search.Core', this)
 		},
 		attach: function(search) {
-			search.setRenderer('comment', this.renderCommentResult.bind(this));
-			search.setHandler('comment', this.handleCommentClick.bind(this));
+			search.setRenderer('comment', this.renderCommentResult.bind(this))
+			search.setHandler('comment', this.handleCommentClick.bind(this))
 		}
-	};
+	}
 
-	OCA.Search.comment = new Comment();
-})(OC, OCA, $);
+	OCA.Search.comment = new Comment()
+})(OC, OCA, $)

+ 5 - 5
apps/files/js/file-upload.js

@@ -727,11 +727,11 @@ OC.Uploader.prototype = _.extend({
 	 *
 	 * @param {array} selection of files to upload
 	 * @param {object} callbacks - object with several callback methods
-	 * @param {function} callbacks.onNoConflicts
-	 * @param {function} callbacks.onSkipConflicts
-	 * @param {function} callbacks.onReplaceConflicts
-	 * @param {function} callbacks.onChooseConflicts
-	 * @param {function} callbacks.onCancel
+	 * @param {Function} callbacks.onNoConflicts
+	 * @param {Function} callbacks.onSkipConflicts
+	 * @param {Function} callbacks.onReplaceConflicts
+	 * @param {Function} callbacks.onChooseConflicts
+	 * @param {Function} callbacks.onCancel
 	 */
 	checkExistingFiles: function (selection, callbacks) {
 		var fileList = this.fileList;

+ 1 - 1
apps/files/js/files.js

@@ -336,7 +336,7 @@
 		 * - JS periodically checks for this cookie and then knows when the download has started to call the callback
 		 *
 		 * @param {string} url download URL
-		 * @param {function} callback function to call once the download has started
+		 * @param {Function} callback function to call once the download has started
 		 */
 		handleDownload: function(url, callback) {
 			var randomToken = Math.random().toString(36).substring(2),

+ 1 - 1
apps/files_external/js/statusmanager.js

@@ -125,7 +125,7 @@ OCA.Files_External.StatusManager = {
 
 	/**
 	 * Function to get external mount point list from the files_external API
-	 * @param {function} afterCallback function to be executed
+	 * @param {Function} afterCallback function to be executed
 	 */
 
 	getMountPointList: function (afterCallback) {

+ 0 - 22
apps/files_sharing/.eslintrc.js

@@ -1,22 +0,0 @@
-module.exports = {
-	env: {
-		browser: true,
-		es6: true
-	},
-	globals: {
-		t: true,
-		n: true,
-		OC: true,
-		OCA: true
-	},
-	extends: 'eslint:recommended',
-	parserOptions: {
-		sourceType: 'module'
-	},
-	rules: {
-		indent: ['error', 'tab'],
-		'linebreak-style': ['error', 'unix'],
-		quotes: ['error', 'single'],
-		semi: ['error', 'always']
-	}
-};

+ 105 - 105
apps/files_sharing/js/app.js

@@ -12,7 +12,7 @@ if (!OCA.Sharing) {
 	/**
 	 * @namespace OCA.Sharing
 	 */
-	OCA.Sharing = {};
+	OCA.Sharing = {}
 }
 /**
  * @namespace
@@ -25,7 +25,7 @@ OCA.Sharing.App = {
 
 	initSharingIn: function($el) {
 		if (this._inFileList) {
-			return this._inFileList;
+			return this._inFileList
 		}
 
 		this._inFileList = new OCA.Sharing.FileList(
@@ -40,19 +40,19 @@ OCA.Sharing.App = {
 				// if handling the event with the file list already created.
 				shown: true
 			}
-		);
+		)
 
-		this._extendFileList(this._inFileList);
-		this._inFileList.appName = t('files_sharing', 'Shared with you');
-		this._inFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>' +
-			'<h2>' + t('files_sharing', 'Nothing shared with you yet') + '</h2>' +
-			'<p>' + t('files_sharing', 'Files and folders others share with you will show up here') + '</p>');
-		return this._inFileList;
+		this._extendFileList(this._inFileList)
+		this._inFileList.appName = t('files_sharing', 'Shared with you')
+		this._inFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>'
+			+ '<h2>' + t('files_sharing', 'Nothing shared with you yet') + '</h2>'
+			+ '<p>' + t('files_sharing', 'Files and folders others share with you will show up here') + '</p>')
+		return this._inFileList
 	},
 
 	initSharingOut: function($el) {
 		if (this._outFileList) {
-			return this._outFileList;
+			return this._outFileList
 		}
 		this._outFileList = new OCA.Sharing.FileList(
 			$el,
@@ -66,19 +66,19 @@ OCA.Sharing.App = {
 				// if handling the event with the file list already created.
 				shown: true
 			}
-		);
+		)
 
-		this._extendFileList(this._outFileList);
-		this._outFileList.appName = t('files_sharing', 'Shared with others');
-		this._outFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>' +
-			'<h2>' + t('files_sharing', 'Nothing shared yet') + '</h2>' +
-			'<p>' + t('files_sharing', 'Files and folders you share will show up here') + '</p>');
-		return this._outFileList;
+		this._extendFileList(this._outFileList)
+		this._outFileList.appName = t('files_sharing', 'Shared with others')
+		this._outFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>'
+			+ '<h2>' + t('files_sharing', 'Nothing shared yet') + '</h2>'
+			+ '<p>' + t('files_sharing', 'Files and folders you share will show up here') + '</p>')
+		return this._outFileList
 	},
 
 	initSharingLinks: function($el) {
 		if (this._linkFileList) {
-			return this._linkFileList;
+			return this._linkFileList
 		}
 		this._linkFileList = new OCA.Sharing.FileList(
 			$el,
@@ -92,19 +92,19 @@ OCA.Sharing.App = {
 				// if handling the event with the file list already created.
 				shown: true
 			}
-		);
+		)
 
-		this._extendFileList(this._linkFileList);
-		this._linkFileList.appName = t('files_sharing', 'Shared by link');
-		this._linkFileList.$el.find('#emptycontent').html('<div class="icon-public"></div>' +
-			'<h2>' + t('files_sharing', 'No shared links') + '</h2>' +
-			'<p>' + t('files_sharing', 'Files and folders you share by link will show up here') + '</p>');
-		return this._linkFileList;
+		this._extendFileList(this._linkFileList)
+		this._linkFileList.appName = t('files_sharing', 'Shared by link')
+		this._linkFileList.$el.find('#emptycontent').html('<div class="icon-public"></div>'
+			+ '<h2>' + t('files_sharing', 'No shared links') + '</h2>'
+			+ '<p>' + t('files_sharing', 'Files and folders you share by link will show up here') + '</p>')
+		return this._linkFileList
 	},
 
 	initSharingDeleted: function($el) {
 		if (this._deletedFileList) {
-			return this._deletedFileList;
+			return this._deletedFileList
 		}
 		this._deletedFileList = new OCA.Sharing.FileList(
 			$el,
@@ -119,19 +119,19 @@ OCA.Sharing.App = {
 				// if handling the event with the file list already created.
 				shown: true
 			}
-		);
+		)
 
-		this._extendFileList(this._deletedFileList);
-		this._deletedFileList.appName = t('files_sharing', 'Deleted shares');
-		this._deletedFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>' +
-			'<h2>' + t('files_sharing', 'No deleted shares') + '</h2>' +
-			'<p>' + t('files_sharing', 'Shares you deleted will show up here') + '</p>');
-		return this._deletedFileList;
+		this._extendFileList(this._deletedFileList)
+		this._deletedFileList.appName = t('files_sharing', 'Deleted shares')
+		this._deletedFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>'
+			+ '<h2>' + t('files_sharing', 'No deleted shares') + '</h2>'
+			+ '<p>' + t('files_sharing', 'Shares you deleted will show up here') + '</p>')
+		return this._deletedFileList
 	},
 
 	initShareingOverview: function($el) {
 		if (this._overviewFileList) {
-			return this._overviewFileList;
+			return this._overviewFileList
 		}
 		this._overviewFileList = new OCA.Sharing.FileList(
 			$el,
@@ -144,43 +144,43 @@ OCA.Sharing.App = {
 				// if handling the event with the file list already created.
 				shown: true
 			}
-		);
+		)
 
-		this._extendFileList(this._overviewFileList);
-		this._overviewFileList.appName = t('files_sharing', 'Shares');
-		this._overviewFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>' +
-			'<h2>' + t('files_sharing', 'No shares') + '</h2>' +
-			'<p>' + t('files_sharing', 'Shares will show up here') + '</p>');
-		return this._overviewFileList;
+		this._extendFileList(this._overviewFileList)
+		this._overviewFileList.appName = t('files_sharing', 'Shares')
+		this._overviewFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>'
+			+ '<h2>' + t('files_sharing', 'No shares') + '</h2>'
+			+ '<p>' + t('files_sharing', 'Shares will show up here') + '</p>')
+		return this._overviewFileList
 	},
 
 	removeSharingIn: function() {
 		if (this._inFileList) {
-			this._inFileList.$fileList.empty();
+			this._inFileList.$fileList.empty()
 		}
 	},
 
 	removeSharingOut: function() {
 		if (this._outFileList) {
-			this._outFileList.$fileList.empty();
+			this._outFileList.$fileList.empty()
 		}
 	},
 
 	removeSharingLinks: function() {
 		if (this._linkFileList) {
-			this._linkFileList.$fileList.empty();
+			this._linkFileList.$fileList.empty()
 		}
 	},
 
 	removeSharingDeleted: function() {
 		if (this._deletedFileList) {
-			this._deletedFileList.$fileList.empty();
+			this._deletedFileList.$fileList.empty()
 		}
 	},
 
 	removeSharingOverview: function() {
 		if (this._overviewFileList) {
-			this._overviewFileList.$fileList.empty();
+			this._overviewFileList.$fileList.empty()
 		}
 	},
 
@@ -188,46 +188,46 @@ OCA.Sharing.App = {
 	 * Destroy the app
 	 */
 	destroy: function() {
-		OCA.Files.fileActions.off('setDefault.app-sharing', this._onActionsUpdated);
-		OCA.Files.fileActions.off('registerAction.app-sharing', this._onActionsUpdated);
-		this.removeSharingIn();
-		this.removeSharingOut();
-		this.removeSharingLinks();
-		this._inFileList = null;
-		this._outFileList = null;
-		this._linkFileList = null;
-		this._overviewFileList = null;
-		delete this._globalActionsInitialized;
+		OCA.Files.fileActions.off('setDefault.app-sharing', this._onActionsUpdated)
+		OCA.Files.fileActions.off('registerAction.app-sharing', this._onActionsUpdated)
+		this.removeSharingIn()
+		this.removeSharingOut()
+		this.removeSharingLinks()
+		this._inFileList = null
+		this._outFileList = null
+		this._linkFileList = null
+		this._overviewFileList = null
+		delete this._globalActionsInitialized
 	},
 
 	_createFileActions: function() {
 		// inherit file actions from the files app
-		var fileActions = new OCA.Files.FileActions();
+		var fileActions = new OCA.Files.FileActions()
 		// note: not merging the legacy actions because legacy apps are not
 		// compatible with the sharing overview and need to be adapted first
-		fileActions.registerDefaultActions();
-		fileActions.merge(OCA.Files.fileActions);
+		fileActions.registerDefaultActions()
+		fileActions.merge(OCA.Files.fileActions)
 
 		if (!this._globalActionsInitialized) {
 			// in case actions are registered later
-			this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
-			OCA.Files.fileActions.on('setDefault.app-sharing', this._onActionsUpdated);
-			OCA.Files.fileActions.on('registerAction.app-sharing', this._onActionsUpdated);
-			this._globalActionsInitialized = true;
+			this._onActionsUpdated = _.bind(this._onActionsUpdated, this)
+			OCA.Files.fileActions.on('setDefault.app-sharing', this._onActionsUpdated)
+			OCA.Files.fileActions.on('registerAction.app-sharing', this._onActionsUpdated)
+			this._globalActionsInitialized = true
 		}
 
 		// when the user clicks on a folder, redirect to the corresponding
 		// folder in the files app instead of opening it directly
-		fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
-			OCA.Files.App.setActiveView('files', {silent: true});
-			OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true);
-		});
-		fileActions.setDefault('dir', 'Open');
-		return fileActions;
+		fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function(filename, context) {
+			OCA.Files.App.setActiveView('files', { silent: true })
+			OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true)
+		})
+		fileActions.setDefault('dir', 'Open')
+		return fileActions
 	},
 
 	_restoreShareAction: function() {
-		var fileActions = new OCA.Files.FileActions();
+		var fileActions = new OCA.Files.FileActions()
 		fileActions.registerAction({
 			name: 'Restore',
 			displayName: '',
@@ -237,70 +237,70 @@ OCA.Sharing.App = {
 			iconClass: 'icon-history',
 			type: OCA.Files.FileActions.TYPE_INLINE,
 			actionHandler: function(fileName, context) {
-				var shareId = context.$file.data('shareId');
+				var shareId = context.$file.data('shareId')
 				$.post(OC.linkToOCS('apps/files_sharing/api/v1/deletedshares', 2) + shareId)
-				.success(function(result) {
-					context.fileList.remove(context.fileInfoModel.attributes.name);
-				}).fail(function() {
-					OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to restore the share.'));
-				});
+					.success(function(result) {
+						context.fileList.remove(context.fileInfoModel.attributes.name)
+					}).fail(function() {
+						OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to restore the share.'))
+					})
 			}
-		});
-		return fileActions;
+		})
+		return fileActions
 	},
 
 	_onActionsUpdated: function(ev) {
 		_.each([this._inFileList, this._outFileList, this._linkFileList], function(list) {
 			if (!list) {
-				return;
+				return
 			}
 
 			if (ev.action) {
-				list.fileActions.registerAction(ev.action);
+				list.fileActions.registerAction(ev.action)
 			} else if (ev.defaultAction) {
 				list.fileActions.setDefault(
 					ev.defaultAction.mime,
 					ev.defaultAction.name
-				);
+				)
 			}
-		});
+		})
 	},
 
 	_extendFileList: function(fileList) {
 		// remove size column from summary
-		fileList.fileSummary.$el.find('.filesize').remove();
+		fileList.fileSummary.$el.find('.filesize').remove()
 	}
-};
+}
 
 $(document).ready(function() {
 	$('#app-content-sharingin').on('show', function(e) {
-		OCA.Sharing.App.initSharingIn($(e.target));
-	});
+		OCA.Sharing.App.initSharingIn($(e.target))
+	})
 	$('#app-content-sharingin').on('hide', function() {
-		OCA.Sharing.App.removeSharingIn();
-	});
+		OCA.Sharing.App.removeSharingIn()
+	})
 	$('#app-content-sharingout').on('show', function(e) {
-		OCA.Sharing.App.initSharingOut($(e.target));
-	});
+		OCA.Sharing.App.initSharingOut($(e.target))
+	})
 	$('#app-content-sharingout').on('hide', function() {
-		OCA.Sharing.App.removeSharingOut();
-	});
+		OCA.Sharing.App.removeSharingOut()
+	})
 	$('#app-content-sharinglinks').on('show', function(e) {
-		OCA.Sharing.App.initSharingLinks($(e.target));
-	});
+		OCA.Sharing.App.initSharingLinks($(e.target))
+	})
 	$('#app-content-sharinglinks').on('hide', function() {
-		OCA.Sharing.App.removeSharingLinks();
-	});
+		OCA.Sharing.App.removeSharingLinks()
+	})
 	$('#app-content-deletedshares').on('show', function(e) {
-		OCA.Sharing.App.initSharingDeleted($(e.target));
-	});
+		OCA.Sharing.App.initSharingDeleted($(e.target))
+	})
 	$('#app-content-deletedshares').on('hide', function() {
-		OCA.Sharing.App.removeSharingDeleted();
-	});
+		OCA.Sharing.App.removeSharingDeleted()
+	})
 	$('#app-content-shareoverview').on('show', function(e) {
-		OCA.Sharing.App.initShareingOverview($(e.target));
-	});
+		OCA.Sharing.App.initShareingOverview($(e.target))
+	})
 	$('#app-content-shareoverview').on('hide', function() {
-		OCA.Sharing.App.removeSharingOverview();
-	});
-});
+		OCA.Sharing.App.removeSharingOverview()
+	})
+})

File diff suppressed because it is too large
+ 0 - 0
apps/files_sharing/js/dist/additionalScripts.js


File diff suppressed because it is too large
+ 0 - 0
apps/files_sharing/js/dist/additionalScripts.js.map


+ 1 - 1
apps/files_sharing/js/dist/collaboration.js

@@ -1,2 +1,2 @@
-!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=0)}([function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n()}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}]);
+!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=0)}([function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}]);
 //# sourceMappingURL=collaboration.js.map

File diff suppressed because it is too large
+ 0 - 0
apps/files_sharing/js/dist/collaboration.js.map


+ 4 - 4
apps/files_sharing/js/dist/files_sharing.4.js

@@ -1,5 +1,5 @@
-(window.webpackJsonpFilesSharing=window.webpackJsonpFilesSharing||[]).push([[4],{14:function(e,o,i){"use strict";i.r(o);var n=i(16),l=i(18),r=i(28),u=i(31),s=i.n(u),a={name:"CollaborationView",computed:{fileId:function(){return this.$root.model&&this.$root.model.id?""+this.$root.model.id:null},filename:function(){return this.$root.model&&this.$root.model.name?""+this.$root.model.name:""}},components:{CollectionList:i(32).a}},d=i(56),c=Object(d.a)(a,(function(){var t=this.$createElement,e=this._self._c||t;return this.fileId?e("collection-list",{attrs:{type:"file",id:this.fileId,name:this.filename}}):this._e()}),[],!1,null,null,null).exports;i.d(o,"Vue",(function(){return n.default})),i.d(o,"View",(function(){return c})),
-/*
+(window.webpackJsonpFilesSharing=window.webpackJsonpFilesSharing||[]).push([[4],{14:function(e,o,i){"use strict";i.r(o);var n=i(16),l=i(18),r=i(28),u=i(31),s=i.n(u),a={name:"CollaborationView",components:{CollectionList:i(32).a},computed:{fileId:function(){return this.$root.model&&this.$root.model.id?""+this.$root.model.id:null},filename:function(){return this.$root.model&&this.$root.model.name?""+this.$root.model.name:""}}},d=i(56),c=Object(d.a)(a,(function(){var t=this.$createElement,e=this._self._c||t;return this.fileId?e("CollectionList",{attrs:{id:this.fileId,type:"file",name:this.filename}}):this._e()}),[],!1,null,null,null).exports;i.d(o,"Vue",(function(){return n.default})),i.d(o,"View",(function(){return c})),
+/**
  * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
  *
  * @author Julius Härtl <jus@bitgrid.net>
@@ -20,5 +20,5 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  */
-n.default.prototype.t=t,n.default.component("PopoverMenu",r.PopoverMenu),n.default.directive("ClickOutside",s.a),r.Tooltip.options.defaultHtml=!1,n.default.directive("Tooltip",r.Tooltip),n.default.use(l.a)}}]);
-//# sourceMappingURL=files_sharing.4.js.map?v=07d5a7a4994f0ef93170
+n.default.prototype.t=t,r.Tooltip.options.defaultHtml=!1,n.default.component("PopoverMenu",r.PopoverMenu),n.default.directive("ClickOutside",s.a),n.default.directive("Tooltip",r.Tooltip),n.default.use(l.a)}}]);
+//# sourceMappingURL=files_sharing.4.js.map?v=4e4a795c94e467758967

File diff suppressed because it is too large
+ 0 - 0
apps/files_sharing/js/dist/files_sharing.4.js.map


File diff suppressed because it is too large
+ 0 - 0
apps/files_sharing/js/dist/files_sharing.js.map


+ 333 - 337
apps/files_sharing/js/sharedfilelist.js

@@ -1,3 +1,4 @@
+/* eslint-disable */
 /*
  * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
  *
@@ -25,428 +26,423 @@
 	 * @param {boolean} [options.linksOnly] true to return only link shares
 	 */
 	var FileList = function($el, options) {
-		this.initialize($el, options);
-	};
+		this.initialize($el, options)
+	}
 	FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
 		/** @lends OCA.Sharing.FileList.prototype */ {
-		appName: 'Shares',
+			appName: 'Shares',
 
-		/**
+			/**
 		 * Whether the list shows the files shared with the user (true) or
 		 * the files that the user shared with others (false).
 		 */
-		_sharedWithUser: false,
-		_linksOnly: false,
-		_showDeleted: false,
-		_clientSideSort: true,
-		_allowSelection: false,
-		_isOverview: false,
-
-		/**
+			_sharedWithUser: false,
+			_linksOnly: false,
+			_showDeleted: false,
+			_clientSideSort: true,
+			_allowSelection: false,
+			_isOverview: false,
+
+			/**
 		 * @private
 		 */
-		initialize: function($el, options) {
-			OCA.Files.FileList.prototype.initialize.apply(this, arguments);
-			if (this.initialized) {
-				return;
-			}
+			initialize: function($el, options) {
+				OCA.Files.FileList.prototype.initialize.apply(this, arguments)
+				if (this.initialized) {
+					return
+				}
 
-			// TODO: consolidate both options
-			if (options && options.sharedWithUser) {
-				this._sharedWithUser = true;
-			}
-			if (options && options.linksOnly) {
-				this._linksOnly = true;
-			}
-			if (options && options.showDeleted) {
-				this._showDeleted = true;
-			}
-			if (options && options.isOverview) {
-				this._isOverview = true;
-			}
-		},
+				// TODO: consolidate both options
+				if (options && options.sharedWithUser) {
+					this._sharedWithUser = true
+				}
+				if (options && options.linksOnly) {
+					this._linksOnly = true
+				}
+				if (options && options.showDeleted) {
+					this._showDeleted = true
+				}
+				if (options && options.isOverview) {
+					this._isOverview = true
+				}
+			},
 
-		_renderRow: function() {
+			_renderRow: function() {
 			// HACK: needed to call the overridden _renderRow
 			// this is because at the time this class is created
 			// the overriding hasn't been done yet...
-			return OCA.Files.FileList.prototype._renderRow.apply(this, arguments);
-		},
+				return OCA.Files.FileList.prototype._renderRow.apply(this, arguments)
+			},
 
-		_createRow: function(fileData) {
+			_createRow: function(fileData) {
 			// TODO: hook earlier and render the whole row here
-			var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments);
-			$tr.find('.filesize').remove();
-			$tr.find('td.date').before($tr.children('td:first'));
-			$tr.find('td.filename input:checkbox').remove();
-			$tr.attr('data-share-id', _.pluck(fileData.shares, 'id').join(','));
-			if (this._sharedWithUser) {
-				$tr.attr('data-share-owner', fileData.shareOwner);
-				$tr.attr('data-mounttype', 'shared-root');
-				var permission = parseInt($tr.attr('data-permissions')) | OC.PERMISSION_DELETE;
-				$tr.attr('data-permissions', permission);
-			}
-			if (this._showDeleted) {
-				var permission = fileData.permissions;
-				$tr.attr('data-share-permissions', permission);
-			}
-
-			// add row with expiration date for link only shares - influenced by _createRow of filelist
-			if (this._linksOnly) {
-				var expirationTimestamp = 0;
-				if(fileData.shares && fileData.shares[0].expiration !== null) {
-					expirationTimestamp = moment(fileData.shares[0].expiration).valueOf();
+				var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments)
+				$tr.find('.filesize').remove()
+				$tr.find('td.date').before($tr.children('td:first'))
+				$tr.find('td.filename input:checkbox').remove()
+				$tr.attr('data-share-id', _.pluck(fileData.shares, 'id').join(','))
+				if (this._sharedWithUser) {
+					$tr.attr('data-share-owner', fileData.shareOwner)
+					$tr.attr('data-mounttype', 'shared-root')
+					var permission = parseInt($tr.attr('data-permissions')) | OC.PERMISSION_DELETE
+					$tr.attr('data-permissions', permission)
 				}
-				$tr.attr('data-expiration', expirationTimestamp);
-
-				// date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
-				// difference in days multiplied by 5 - brightest shade for expiry dates in more than 32 days (160/5)
-				var modifiedColor = Math.round((expirationTimestamp - (new Date()).getTime()) / 1000 / 60 / 60 / 24 * 5);
-				// ensure that the brightest color is still readable
-				if (modifiedColor >= 160) {
-					modifiedColor = 160;
+				if (this._showDeleted) {
+					var permission = fileData.permissions
+					$tr.attr('data-share-permissions', permission)
 				}
 
-				var formatted;
-				var text;
-				if (expirationTimestamp > 0) {
-					formatted = OC.Util.formatDate(expirationTimestamp);
-					text = OC.Util.relativeModifiedDate(expirationTimestamp);
-				} else {
-					formatted = t('files_sharing', 'No expiration date set');
-					text = '';
-					modifiedColor = 160;
-				}
-				td = $('<td></td>').attr({"class": "date"});
-				td.append($('<span></span>').attr({
-						"class": "modified",
-						"title": formatted,
-						"style": 'color:rgb(' + modifiedColor + ',' + modifiedColor + ',' + modifiedColor + ')'
+				// add row with expiration date for link only shares - influenced by _createRow of filelist
+				if (this._linksOnly) {
+					var expirationTimestamp = 0
+					if (fileData.shares && fileData.shares[0].expiration !== null) {
+						expirationTimestamp = moment(fileData.shares[0].expiration).valueOf()
+					}
+					$tr.attr('data-expiration', expirationTimestamp)
+
+					// date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
+					// difference in days multiplied by 5 - brightest shade for expiry dates in more than 32 days (160/5)
+					var modifiedColor = Math.round((expirationTimestamp - (new Date()).getTime()) / 1000 / 60 / 60 / 24 * 5)
+					// ensure that the brightest color is still readable
+					if (modifiedColor >= 160) {
+						modifiedColor = 160
+					}
+
+					var formatted
+					var text
+					if (expirationTimestamp > 0) {
+						formatted = OC.Util.formatDate(expirationTimestamp)
+						text = OC.Util.relativeModifiedDate(expirationTimestamp)
+					} else {
+						formatted = t('files_sharing', 'No expiration date set')
+						text = ''
+						modifiedColor = 160
+					}
+					td = $('<td></td>').attr({ 'class': 'date' })
+					td.append($('<span></span>').attr({
+						'class': 'modified',
+						'title': formatted,
+						'style': 'color:rgb(' + modifiedColor + ',' + modifiedColor + ',' + modifiedColor + ')'
 					}).text(text)
-						.tooltip({placement: 'top'})
-				);
+						.tooltip({ placement: 'top' })
+					)
 
-				$tr.append(td);
-			}
-			return $tr;
-		},
+					$tr.append(td)
+				}
+				return $tr
+			},
 
-		/**
+			/**
 		 * Set whether the list should contain outgoing shares
 		 * or incoming shares.
 		 *
 		 * @param state true for incoming shares, false otherwise
 		 */
-		setSharedWithUser: function(state) {
-			this._sharedWithUser = !!state;
-		},
+			setSharedWithUser: function(state) {
+				this._sharedWithUser = !!state
+			},
 
-		updateEmptyContent: function() {
-			var dir = this.getCurrentDirectory();
-			if (dir === '/') {
+			updateEmptyContent: function() {
+				var dir = this.getCurrentDirectory()
+				if (dir === '/') {
 				// root has special permissions
-				this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
-				this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
+					this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty)
+					this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty)
 
-				// hide expiration date header for non link only shares
-				if (!this._linksOnly) {
-					this.$el.find('th.column-expiration').addClass('hidden');
+					// hide expiration date header for non link only shares
+					if (!this._linksOnly) {
+						this.$el.find('th.column-expiration').addClass('hidden')
+					}
+				} else {
+					OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments)
 				}
-			}
-			else {
-				OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
-			}
-		},
+			},
 
-		getDirectoryPermissions: function() {
-			return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
-		},
+			getDirectoryPermissions: function() {
+				return OC.PERMISSION_READ | OC.PERMISSION_DELETE
+			},
 
-		updateStorageStatistics: function() {
+			updateStorageStatistics: function() {
 			// no op because it doesn't have
 			// storage info like free space / used space
-		},
+			},
 
-		updateRow: function($tr, fileInfo, options) {
+			updateRow: function($tr, fileInfo, options) {
 			// no-op, suppress re-rendering
-			return $tr;
-		},
+				return $tr
+			},
 
-		reload: function() {
-			this.showMask();
-			if (this._reloadCall) {
-				this._reloadCall.abort();
-			}
+			reload: function() {
+				this.showMask()
+				if (this._reloadCall) {
+					this._reloadCall.abort()
+				}
 
-			// there is only root
-			this._setCurrentDir('/', false);
-
-			var promises = [];
-
-			var deletedShares = {
-				url: OC.linkToOCS('apps/files_sharing/api/v1', 2) + 'deletedshares',
-				/* jshint camelcase: false */
-				data: {
-					format: 'json',
-					include_tags: true
-				},
-				type: 'GET',
-				beforeSend: function (xhr) {
-					xhr.setRequestHeader('OCS-APIREQUEST', 'true');
-				},
-			};
-
-			var shares = {
-				url: OC.linkToOCS('apps/files_sharing/api/v1') + 'shares',
-				/* jshint camelcase: false */
-				data: {
-					format: 'json',
-					shared_with_me: this._sharedWithUser !== false,
-					include_tags: true
-				},
-				type: 'GET',
-				beforeSend: function (xhr) {
-					xhr.setRequestHeader('OCS-APIREQUEST', 'true');
-				},
-			};
-
-			var remoteShares = {
-				url: OC.linkToOCS('apps/files_sharing/api/v1') + 'remote_shares',
-				/* jshint camelcase: false */
-				data: {
-					format: 'json',
-					include_tags: true
-				},
-				type: 'GET',
-				beforeSend: function (xhr) {
-					xhr.setRequestHeader('OCS-APIREQUEST', 'true');
-				},
-			};
-
-			// Add the proper ajax requests to the list and run them
-			// and make sure we have 2 promises
-			if (this._showDeleted) {
-				promises.push($.ajax(deletedShares));
-			} else {
-				promises.push($.ajax(shares));
-
-				if (this._sharedWithUser !== false || this._isOverview) {
-					promises.push($.ajax(remoteShares));
+				// there is only root
+				this._setCurrentDir('/', false)
+
+				var promises = []
+
+				var deletedShares = {
+					url: OC.linkToOCS('apps/files_sharing/api/v1', 2) + 'deletedshares',
+					/* jshint camelcase: false */
+					data: {
+						format: 'json',
+						include_tags: true
+					},
+					type: 'GET',
+					beforeSend: function(xhr) {
+						xhr.setRequestHeader('OCS-APIREQUEST', 'true')
+					}
 				}
-				if (this._isOverview) {
-					shares.data.shared_with_me = !shares.data.shared_with_me;
-					promises.push($.ajax(shares));
+
+				var shares = {
+					url: OC.linkToOCS('apps/files_sharing/api/v1') + 'shares',
+					/* jshint camelcase: false */
+					data: {
+						format: 'json',
+						shared_with_me: this._sharedWithUser !== false,
+						include_tags: true
+					},
+					type: 'GET',
+					beforeSend: function(xhr) {
+						xhr.setRequestHeader('OCS-APIREQUEST', 'true')
+					}
+				}
+
+				var remoteShares = {
+					url: OC.linkToOCS('apps/files_sharing/api/v1') + 'remote_shares',
+					/* jshint camelcase: false */
+					data: {
+						format: 'json',
+						include_tags: true
+					},
+					type: 'GET',
+					beforeSend: function(xhr) {
+						xhr.setRequestHeader('OCS-APIREQUEST', 'true')
+					}
 				}
-			}
 
-			this._reloadCall = $.when.apply($, promises);
-			var callBack = this.reloadCallback.bind(this);
-			return this._reloadCall.then(callBack, callBack);
-		},
+				// Add the proper ajax requests to the list and run them
+				// and make sure we have 2 promises
+				if (this._showDeleted) {
+					promises.push($.ajax(deletedShares))
+				} else {
+					promises.push($.ajax(shares))
 
-		reloadCallback: function(shares, remoteShares, additionalShares) {
-			delete this._reloadCall;
-			this.hideMask();
+					if (this._sharedWithUser !== false || this._isOverview) {
+						promises.push($.ajax(remoteShares))
+					}
+					if (this._isOverview) {
+						shares.data.shared_with_me = !shares.data.shared_with_me
+						promises.push($.ajax(shares))
+					}
+				}
 
-			this.$el.find('#headerSharedWith').text(
-				t('files_sharing', this._sharedWithUser ? 'Shared by' : 'Shared with')
-			);
+				this._reloadCall = $.when.apply($, promises)
+				var callBack = this.reloadCallback.bind(this)
+				return this._reloadCall.then(callBack, callBack)
+			},
 
-			var files = [];
+			reloadCallback: function(shares, remoteShares, additionalShares) {
+				delete this._reloadCall
+				this.hideMask()
 
-			// make sure to use the same format
-			if (shares[0] && shares[0].ocs) {
-				shares = shares[0];
-			}
-			if (remoteShares && remoteShares[0] && remoteShares[0].ocs) {
-				remoteShares = remoteShares[0];
-			}
-			if (additionalShares && additionalShares[0] && additionalShares[0].ocs) {
-				additionalShares = additionalShares[0];
-			}
+				this.$el.find('#headerSharedWith').text(
+					t('files_sharing', this._sharedWithUser ? 'Shared by' : 'Shared with')
+				)
 
-			if (shares.ocs && shares.ocs.data) {
-				files = files.concat(this._makeFilesFromShares(shares.ocs.data, this._sharedWithUser));
-			}
+				var files = []
 
-			if (remoteShares && remoteShares.ocs && remoteShares.ocs.data) {
-				files = files.concat(this._makeFilesFromRemoteShares(remoteShares.ocs.data));
-			}
+				// make sure to use the same format
+				if (shares[0] && shares[0].ocs) {
+					shares = shares[0]
+				}
+				if (remoteShares && remoteShares[0] && remoteShares[0].ocs) {
+					remoteShares = remoteShares[0]
+				}
+				if (additionalShares && additionalShares[0] && additionalShares[0].ocs) {
+					additionalShares = additionalShares[0]
+				}
 
-			if (additionalShares && additionalShares.ocs && additionalShares.ocs.data) {
-				files = files.concat(this._makeFilesFromShares(additionalShares.ocs.data, !this._sharedWithUser));
-			}
+				if (shares.ocs && shares.ocs.data) {
+					files = files.concat(this._makeFilesFromShares(shares.ocs.data, this._sharedWithUser))
+				}
+
+				if (remoteShares && remoteShares.ocs && remoteShares.ocs.data) {
+					files = files.concat(this._makeFilesFromRemoteShares(remoteShares.ocs.data))
+				}
 
+				if (additionalShares && additionalShares.ocs && additionalShares.ocs.data) {
+					files = files.concat(this._makeFilesFromShares(additionalShares.ocs.data, !this._sharedWithUser))
+				}
 
-			this.setFiles(files);
-			return true;
-		},
+				this.setFiles(files)
+				return true
+			},
 
-		_makeFilesFromRemoteShares: function(data) {
-			var files = data;
+			_makeFilesFromRemoteShares: function(data) {
+				var files = data
 
-			files = _.chain(files)
+				files = _.chain(files)
 				// convert share data to file data
-				.map(function(share) {
-					var file = {
-						shareOwner: share.owner + '@' + share.remote.replace(/.*?:\/\//g, ""),
-						name: OC.basename(share.mountpoint),
-						mtime: share.mtime * 1000,
-						mimetype: share.mimetype,
-						type: share.type,
-						id: share.file_id,
-						path: OC.dirname(share.mountpoint),
-						permissions: share.permissions,
-						tags: share.tags || []
-					};
-
-					file.shares = [{
-						id: share.id,
-						type: OC.Share.SHARE_TYPE_REMOTE
-					}];
-					return file;
-				})
-				.value();
-			return files;
-		},
-
-		/**
+					.map(function(share) {
+						var file = {
+							shareOwner: share.owner + '@' + share.remote.replace(/.*?:\/\//g, ''),
+							name: OC.basename(share.mountpoint),
+							mtime: share.mtime * 1000,
+							mimetype: share.mimetype,
+							type: share.type,
+							id: share.file_id,
+							path: OC.dirname(share.mountpoint),
+							permissions: share.permissions,
+							tags: share.tags || []
+						}
+
+						file.shares = [{
+							id: share.id,
+							type: OC.Share.SHARE_TYPE_REMOTE
+						}]
+						return file
+					})
+					.value()
+				return files
+			},
+
+			/**
 		 * Converts the OCS API share response data to a file info
 		 * list
 		 * @param {Array} data OCS API share array
 		 * @param {bool} sharedWithUser
-		 * @return {Array.<OCA.Sharing.SharedFileInfo>} array of shared file info
+		 * @returns {Array.<OCA.Sharing.SharedFileInfo>} array of shared file info
 		 */
-		_makeFilesFromShares: function(data, sharedWithUser) {
+			_makeFilesFromShares: function(data, sharedWithUser) {
 			/* jshint camelcase: false */
-			var files = data;
+				var files = data
 
-			if (this._linksOnly) {
-				files = _.filter(data, function(share) {
-					return share.share_type === OC.Share.SHARE_TYPE_LINK;
-				});
-			}
+				if (this._linksOnly) {
+					files = _.filter(data, function(share) {
+						return share.share_type === OC.Share.SHARE_TYPE_LINK
+					})
+				}
 
-			// OCS API uses non-camelcased names
-			files = _.chain(files)
+				// OCS API uses non-camelcased names
+				files = _.chain(files)
 				// convert share data to file data
-				.map(function(share) {
+					.map(function(share) {
 					// TODO: use OC.Files.FileInfo
-					var file = {
-						id: share.file_source,
-						icon: OC.MimeType.getIconUrl(share.mimetype),
-						mimetype: share.mimetype,
-						tags: share.tags || []
-					};
-					if (share.item_type === 'folder') {
-						file.type = 'dir';
-						file.mimetype = 'httpd/unix-directory';
-					}
-					else {
-						file.type = 'file';
-					}
-					file.share = {
-						id: share.id,
-						type: share.share_type,
-						target: share.share_with,
-						stime: share.stime * 1000,
-						expiration: share.expiration,
-					};
-					if (sharedWithUser) {
-						file.shareOwner = share.displayname_owner;
-						file.shareOwnerId = share.uid_owner;
-						file.name = OC.basename(share.file_target);
-						file.path = OC.dirname(share.file_target);
-						file.permissions = share.permissions;
-						if (file.path) {
-							file.extraData = share.file_target;
+						var file = {
+							id: share.file_source,
+							icon: OC.MimeType.getIconUrl(share.mimetype),
+							mimetype: share.mimetype,
+							tags: share.tags || []
 						}
-					}
-					else {
-						if (share.share_type !== OC.Share.SHARE_TYPE_LINK) {
-							file.share.targetDisplayName = share.share_with_displayname;
-							file.share.targetShareWithId = share.share_with;
+						if (share.item_type === 'folder') {
+							file.type = 'dir'
+							file.mimetype = 'httpd/unix-directory'
+						} else {
+							file.type = 'file'
 						}
-						file.name = OC.basename(share.path);
-						file.path = OC.dirname(share.path);
-						file.permissions = OC.PERMISSION_ALL;
-						if (file.path) {
-							file.extraData = share.path;
+						file.share = {
+							id: share.id,
+							type: share.share_type,
+							target: share.share_with,
+							stime: share.stime * 1000,
+							expiration: share.expiration
 						}
-					}
-					return file;
-				})
+						if (sharedWithUser) {
+							file.shareOwner = share.displayname_owner
+							file.shareOwnerId = share.uid_owner
+							file.name = OC.basename(share.file_target)
+							file.path = OC.dirname(share.file_target)
+							file.permissions = share.permissions
+							if (file.path) {
+								file.extraData = share.file_target
+							}
+						} else {
+							if (share.share_type !== OC.Share.SHARE_TYPE_LINK) {
+								file.share.targetDisplayName = share.share_with_displayname
+								file.share.targetShareWithId = share.share_with
+							}
+							file.name = OC.basename(share.path)
+							file.path = OC.dirname(share.path)
+							file.permissions = OC.PERMISSION_ALL
+							if (file.path) {
+								file.extraData = share.path
+							}
+						}
+						return file
+					})
 				// Group all files and have a "shares" array with
 				// the share info for each file.
 				//
 				// This uses a hash memo to cumulate share information
 				// inside the same file object (by file id).
-				.reduce(function(memo, file) {
-					var data = memo[file.id];
-					var recipient = file.share.targetDisplayName;
-					var recipientId = file.share.targetShareWithId;
-					if (!data) {
-						data = memo[file.id] = file;
-						data.shares = [file.share];
-						// using a hash to make them unique,
-						// this is only a list to be displayed
-						data.recipients = {};
-						data.recipientData = {};
-						// share types
-						data.shareTypes = {};
-						// counter is cheaper than calling _.keys().length
-						data.recipientsCount = 0;
-						data.mtime = file.share.stime;
-					}
-					else {
+					.reduce(function(memo, file) {
+						var data = memo[file.id]
+						var recipient = file.share.targetDisplayName
+						var recipientId = file.share.targetShareWithId
+						if (!data) {
+							data = memo[file.id] = file
+							data.shares = [file.share]
+							// using a hash to make them unique,
+							// this is only a list to be displayed
+							data.recipients = {}
+							data.recipientData = {}
+							// share types
+							data.shareTypes = {}
+							// counter is cheaper than calling _.keys().length
+							data.recipientsCount = 0
+							data.mtime = file.share.stime
+						} else {
 						// always take the most recent stime
-						if (file.share.stime > data.mtime) {
-							data.mtime = file.share.stime;
+							if (file.share.stime > data.mtime) {
+								data.mtime = file.share.stime
+							}
+							data.shares.push(file.share)
 						}
-						data.shares.push(file.share);
-					}
 
-					if (recipient) {
+						if (recipient) {
 						// limit counterparts for output
-						if (data.recipientsCount < 4) {
+							if (data.recipientsCount < 4) {
 							// only store the first ones, they will be the only ones
 							// displayed
-							data.recipients[recipient] = true;
-							data.recipientData[data.recipientsCount] = {
-								'shareWith': recipientId,
-								'shareWithDisplayName': recipient
-							};
+								data.recipients[recipient] = true
+								data.recipientData[data.recipientsCount] = {
+									'shareWith': recipientId,
+									'shareWithDisplayName': recipient
+								}
+							}
+							data.recipientsCount++
 						}
-						data.recipientsCount++;
-					}
 
-					data.shareTypes[file.share.type] = true;
+						data.shareTypes[file.share.type] = true
 
-					delete file.share;
-					return memo;
-				}, {})
+						delete file.share
+						return memo
+					}, {})
 				// Retrieve only the values of the returned hash
-				.values()
+					.values()
 				// Clean up
-				.each(function(data) {
+					.each(function(data) {
 					// convert the recipients map to a flat
 					// array of sorted names
-					data.mountType = 'shared';
-					delete data.recipientsCount;
-					if (sharedWithUser) {
+						data.mountType = 'shared'
+						delete data.recipientsCount
+						if (sharedWithUser) {
 						// only for outgoing shares
-						delete data.shareTypes;
-					} else {
-						data.shareTypes = _.keys(data.shareTypes);
-					}
-				})
+							delete data.shareTypes
+						} else {
+							data.shareTypes = _.keys(data.shareTypes)
+						}
+					})
 				// Finish the chain by getting the result
-				.value();
+					.value()
 
-			// Sort by expected sort comparator
-			return files.sort(this._sortComparator);
-		},
-	});
+				// Sort by expected sort comparator
+				return files.sort(this._sortComparator)
+			}
+		})
 
 	/**
 	 * Share info attributes.
@@ -486,5 +482,5 @@
 	 * passing to HTML data attributes with jQuery)
 	 */
 
-	OCA.Sharing.FileList = FileList;
-})();
+	OCA.Sharing.FileList = FileList
+})()

+ 6 - 4
apps/files_sharing/src/additionalScripts.js

@@ -1,6 +1,3 @@
-__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/');
-__webpack_nonce__ = btoa(OC.requestToken);
-
 import './share'
 import './sharetabview'
 import './sharebreadcrumbview'
@@ -10,4 +7,9 @@ import './style/sharebreadcrumb.scss'
 
 import './collaborationresourceshandler.js'
 
-window.OCA.Sharing = OCA.Sharing;
+// eslint-disable-next-line camelcase
+__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = btoa(OC.requestToken)
+
+window.OCA.Sharing = OCA.Sharing

+ 14 - 12
apps/files_sharing/src/collaborationresources.js

@@ -1,4 +1,4 @@
-/*
+/**
  * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
  *
  * @author Julius Härtl <jus@bitgrid.net>
@@ -20,21 +20,23 @@
  *
  */
 
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { Tooltip, PopoverMenu } from 'nextcloud-vue';
-import ClickOutside from 'vue-click-outside';
+import Vue from 'vue'
+import Vuex from 'vuex'
+import { Tooltip, PopoverMenu } from 'nextcloud-vue'
+import ClickOutside from 'vue-click-outside'
 
-Vue.prototype.t = t;
-Vue.component('PopoverMenu', PopoverMenu);
-Vue.directive('ClickOutside', ClickOutside);
+import View from './views/CollaborationView'
+
+Vue.prototype.t = t
 Tooltip.options.defaultHtml = false
-Vue.directive('Tooltip', Tooltip);
-Vue.use(Vuex);
 
-import View from './views/CollaborationView';
+// eslint-disable-next-line vue/match-component-file-name
+Vue.component('PopoverMenu', PopoverMenu)
+Vue.directive('ClickOutside', ClickOutside)
+Vue.directive('Tooltip', Tooltip)
+Vue.use(Vuex)
 
 export {
 	Vue,
 	View
-};
+}

+ 12 - 10
apps/files_sharing/src/collaborationresourceshandler.js

@@ -1,19 +1,21 @@
-__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/');
-__webpack_nonce__ = btoa(OC.requestToken);
+// eslint-disable-next-line camelcase
+__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = btoa(OC.requestToken)
 
 window.OCP.Collaboration.registerType('file', {
 	action: () => {
 		return new Promise((resolve, reject) => {
-			OC.dialogs.filepicker(t('files_sharing', 'Link to a file'), function (f) {
-				const client = OC.Files.getClient();
+			OC.dialogs.filepicker(t('files_sharing', 'Link to a file'), function(f) {
+				const client = OC.Files.getClient()
 				client.getFileInfo(f).then((status, fileInfo) => {
-					resolve(fileInfo.id);
+					resolve(fileInfo.id)
 				}).fail(() => {
-					reject();
-				});
-			}, false, null, false, OC.dialogs.FILEPICKER_TYPE_CHOOSE, '', { allowDirectoryChooser: true });
-		});
+					reject(new Error('Cannot get fileinfo'))
+				})
+			}, false, null, false, OC.dialogs.FILEPICKER_TYPE_CHOOSE, '', { allowDirectoryChooser: true })
+		})
 	},
 	typeString: t('files_sharing', 'Link to a file'),
 	typeIconClass: 'icon-files-dark'
-});
+})

+ 6 - 4
apps/files_sharing/src/files_sharing.js

@@ -1,5 +1,7 @@
-__webpack_nonce__ = btoa(OC.requestToken);
-__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/');
+import '../js/app'
+import '../js/sharedfilelist'
 
-import '../js/app';
-import '../js/sharedfilelist';
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = btoa(OC.requestToken)
+// eslint-disable-next-line camelcase
+__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')

+ 109 - 110
apps/files_sharing/src/share.js

@@ -1,3 +1,4 @@
+/* eslint-disable */
 /*
  * Copyright (c) 2014
  *
@@ -14,10 +15,10 @@
 		PROPERTY_SHARE_TYPES:	'{' + OC.Files.Client.NS_OWNCLOUD + '}share-types',
 		PROPERTY_OWNER_ID:	'{' + OC.Files.Client.NS_OWNCLOUD + '}owner-id',
 		PROPERTY_OWNER_DISPLAY_NAME:	'{' + OC.Files.Client.NS_OWNCLOUD + '}owner-display-name'
-	});
+	})
 
 	if (!OCA.Sharing) {
-		OCA.Sharing = {};
+		OCA.Sharing = {}
 	}
 	/**
 	 * @namespace
@@ -34,131 +35,130 @@
 		attach: function(fileList) {
 			// core sharing is disabled/not loaded
 			if (!OC.Share) {
-				return;
+				return
 			}
 			if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
-				return;
+				return
 			}
-			var fileActions = fileList.fileActions;
-			var oldCreateRow = fileList._createRow;
+			var fileActions = fileList.fileActions
+			var oldCreateRow = fileList._createRow
 			fileList._createRow = function(fileData) {
 
-				var tr = oldCreateRow.apply(this, arguments);
-				var sharePermissions = OCA.Sharing.Util.getSharePermissions(fileData);
-				
+				var tr = oldCreateRow.apply(this, arguments)
+				var sharePermissions = OCA.Sharing.Util.getSharePermissions(fileData)
+
 				if (fileData.permissions === 0) {
 					// no permission, disabling sidebar
-					delete fileActions.actions.all.Comment;
-					delete fileActions.actions.all.Details;
-					delete fileActions.actions.all.Goto;
+					delete fileActions.actions.all.Comment
+					delete fileActions.actions.all.Details
+					delete fileActions.actions.all.Goto
 				}
-				tr.attr('data-share-permissions', sharePermissions);
+				tr.attr('data-share-permissions', sharePermissions)
 				if (fileData.shareOwner) {
-					tr.attr('data-share-owner', fileData.shareOwner);
-					tr.attr('data-share-owner-id', fileData.shareOwnerId);
+					tr.attr('data-share-owner', fileData.shareOwner)
+					tr.attr('data-share-owner-id', fileData.shareOwnerId)
 					// user should always be able to rename a mount point
 					if (fileData.mountType === 'shared-root') {
-						tr.attr('data-permissions', fileData.permissions | OC.PERMISSION_UPDATE);
+						tr.attr('data-permissions', fileData.permissions | OC.PERMISSION_UPDATE)
 					}
 				}
 				if (fileData.recipientData && !_.isEmpty(fileData.recipientData)) {
-					tr.attr('data-share-recipient-data', JSON.stringify(fileData.recipientData));
+					tr.attr('data-share-recipient-data', JSON.stringify(fileData.recipientData))
 				}
 				if (fileData.shareTypes) {
-					tr.attr('data-share-types', fileData.shareTypes.join(','));
+					tr.attr('data-share-types', fileData.shareTypes.join(','))
 				}
-				return tr;
-			};
+				return tr
+			}
 
-			var oldElementToFile = fileList.elementToFile;
+			var oldElementToFile = fileList.elementToFile
 			fileList.elementToFile = function($el) {
-				var fileInfo = oldElementToFile.apply(this, arguments);
-				fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined;
-				fileInfo.shareOwner = $el.attr('data-share-owner') || undefined;
-				fileInfo.shareOwnerId = $el.attr('data-share-owner-id') || undefined;
+				var fileInfo = oldElementToFile.apply(this, arguments)
+				fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined
+				fileInfo.shareOwner = $el.attr('data-share-owner') || undefined
+				fileInfo.shareOwnerId = $el.attr('data-share-owner-id') || undefined
 
-				if( $el.attr('data-share-types')){
-					fileInfo.shareTypes = $el.attr('data-share-types').split(',');
+				if ($el.attr('data-share-types')) {
+					fileInfo.shareTypes = $el.attr('data-share-types').split(',')
 				}
 
-				if( $el.attr('data-expiration')){
-					var expirationTimestamp = parseInt($el.attr('data-expiration'));
-					fileInfo.shares = [];
-					fileInfo.shares.push({expiration: expirationTimestamp});
+				if ($el.attr('data-expiration')) {
+					var expirationTimestamp = parseInt($el.attr('data-expiration'))
+					fileInfo.shares = []
+					fileInfo.shares.push({ expiration: expirationTimestamp })
 				}
 
-				return fileInfo;
-			};
+				return fileInfo
+			}
 
-			var oldGetWebdavProperties = fileList._getWebdavProperties;
+			var oldGetWebdavProperties = fileList._getWebdavProperties
 			fileList._getWebdavProperties = function() {
-				var props = oldGetWebdavProperties.apply(this, arguments);
-				props.push(OC.Files.Client.PROPERTY_OWNER_ID);
-				props.push(OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME);
-				props.push(OC.Files.Client.PROPERTY_SHARE_TYPES);
-				return props;
-			};
+				var props = oldGetWebdavProperties.apply(this, arguments)
+				props.push(OC.Files.Client.PROPERTY_OWNER_ID)
+				props.push(OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME)
+				props.push(OC.Files.Client.PROPERTY_SHARE_TYPES)
+				return props
+			}
 
 			fileList.filesClient.addFileInfoParser(function(response) {
-				var data = {};
-				var props = response.propStat[0].properties;
-				var permissionsProp = props[OC.Files.Client.PROPERTY_PERMISSIONS];
+				var data = {}
+				var props = response.propStat[0].properties
+				var permissionsProp = props[OC.Files.Client.PROPERTY_PERMISSIONS]
 
 				if (permissionsProp && permissionsProp.indexOf('S') >= 0) {
-					data.shareOwner = props[OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME];
-					data.shareOwnerId = props[OC.Files.Client.PROPERTY_OWNER_ID];
+					data.shareOwner = props[OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME]
+					data.shareOwnerId = props[OC.Files.Client.PROPERTY_OWNER_ID]
 				}
 
-				var shareTypesProp = props[OC.Files.Client.PROPERTY_SHARE_TYPES];
+				var shareTypesProp = props[OC.Files.Client.PROPERTY_SHARE_TYPES]
 				if (shareTypesProp) {
 					data.shareTypes = _.chain(shareTypesProp).filter(function(xmlvalue) {
-						return (xmlvalue.namespaceURI === OC.Files.Client.NS_OWNCLOUD && xmlvalue.nodeName.split(':')[1] === 'share-type');
+						return (xmlvalue.namespaceURI === OC.Files.Client.NS_OWNCLOUD && xmlvalue.nodeName.split(':')[1] === 'share-type')
 					}).map(function(xmlvalue) {
-						return parseInt(xmlvalue.textContent || xmlvalue.text, 10);
-					}).value();
+						return parseInt(xmlvalue.textContent || xmlvalue.text, 10)
+					}).value()
 				}
 
-				return data;
-			});
+				return data
+			})
 
 			// use delegate to catch the case with multiple file lists
-			fileList.$el.on('fileActionsReady', function(ev){
-				var $files = ev.$files;
+			fileList.$el.on('fileActionsReady', function(ev) {
+				var $files = ev.$files
 
 				_.each($files, function(file) {
-					var $tr = $(file);
-					var shareTypes = $tr.attr('data-share-types') || '';
-					var shareOwner = $tr.attr('data-share-owner');
+					var $tr = $(file)
+					var shareTypes = $tr.attr('data-share-types') || ''
+					var shareOwner = $tr.attr('data-share-owner')
 					if (shareTypes || shareOwner) {
-						var hasLink = false;
-						var hasShares = false;
+						var hasLink = false
+						var hasShares = false
 						_.each(shareTypes.split(',') || [], function(shareType) {
-							shareType = parseInt(shareType, 10);
+							shareType = parseInt(shareType, 10)
 							if (shareType === OC.Share.SHARE_TYPE_LINK) {
-								hasLink = true;
+								hasLink = true
 							} else if (shareType === OC.Share.SHARE_TYPE_EMAIL) {
-								hasLink = true;
+								hasLink = true
 							} else if (shareType === OC.Share.SHARE_TYPE_USER) {
-								hasShares = true;
+								hasShares = true
 							} else if (shareType === OC.Share.SHARE_TYPE_GROUP) {
-								hasShares = true;
+								hasShares = true
 							} else if (shareType === OC.Share.SHARE_TYPE_REMOTE) {
-								hasShares = true;
+								hasShares = true
 							} else if (shareType === OC.Share.SHARE_TYPE_CIRCLE) {
-								hasShares = true;
+								hasShares = true
 							} else if (shareType === OC.Share.SHARE_TYPE_ROOM) {
-								hasShares = true;
+								hasShares = true
 							}
-						});
-						OCA.Sharing.Util._updateFileActionIcon($tr, hasShares, hasLink);
+						})
+						OCA.Sharing.Util._updateFileActionIcon($tr, hasShares, hasLink)
 					}
-				});
-			});
-
+				})
+			})
 
 			fileList.$el.on('changeDirectory', function() {
-				OCA.Sharing.sharesLoaded = false;
-			});
+				OCA.Sharing.sharesLoaded = false
+			})
 
 			fileActions.registerAction({
 				name: 'Share',
@@ -193,40 +193,40 @@
 				type: OCA.Files.FileActions.TYPE_INLINE,
 				actionHandler: function(fileName, context) {
 					// do not open sidebar if permission is set and equal to 0
-					var permissions = parseInt(context.$file.data('share-permissions'), 10);
+					var permissions = parseInt(context.$file.data('share-permissions'), 10)
 					if (isNaN(permissions) || permissions > 0) {
-						fileList.showDetailsView(fileName, 'shareTabView');
+						fileList.showDetailsView(fileName, 'shareTabView')
 					}
 				},
 				render: function(actionSpec, isDefault, context) {
-					var permissions = parseInt(context.$file.data('permissions'), 10);
+					var permissions = parseInt(context.$file.data('permissions'), 10)
 					// if no share permissions but share owner exists, still show the link
 					if ((permissions & OC.PERMISSION_SHARE) !== 0 || context.$file.attr('data-share-owner')) {
-						return fileActions._defaultRenderAction.call(fileActions, actionSpec, isDefault, context);
+						return fileActions._defaultRenderAction.call(fileActions, actionSpec, isDefault, context)
 					}
 					// don't render anything
-					return null;
+					return null
 				}
-			});
+			})
 
-			var shareTab = new OCA.Sharing.ShareTabView('shareTabView', {order: -20});
+			var shareTab = new OCA.Sharing.ShareTabView('shareTabView', { order: -20 })
 			// detect changes and change the matching list entry
 			shareTab.on('sharesChanged', function(shareModel) {
-				var fileInfoModel = shareModel.fileInfoModel;
-				var $tr = fileList.findFileEl(fileInfoModel.get('name'));
+				var fileInfoModel = shareModel.fileInfoModel
+				var $tr = fileList.findFileEl(fileInfoModel.get('name'))
 
 				// We count email shares as link share
-				var hasLinkShares = shareModel.hasLinkShares();
-				shareModel.get('shares').forEach(function (share) {
+				var hasLinkShares = shareModel.hasLinkShares()
+				shareModel.get('shares').forEach(function(share) {
 					if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
-						hasLinkShares = true;
+						hasLinkShares = true
 					}
-				});
+				})
 
-				OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel);
+				OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel)
 				if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) {
 					// remove icon, if applicable
-					OC.Share.markFileAsShared($tr, false, false);
+					OC.Share.markFileAsShared($tr, false, false)
 				}
 
 				// FIXME: this is too convoluted. We need to get rid of the above updates
@@ -237,12 +237,12 @@
 					// we need to modify the model
 					// (FIXME: yes, this is hacky)
 					icon: $tr.attr('data-icon')
-				});
-			});
-			fileList.registerTabView(shareTab);
+				})
+			})
+			fileList.registerTabView(shareTab)
 
-			var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({shareTab: shareTab});
-			fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView);
+			var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab })
+			fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView)
 		},
 
 		/**
@@ -252,18 +252,17 @@
 			// files app current cannot show recipients on load, so we don't update the
 			// icon when changed for consistency
 			if (fileList.id === 'files') {
-				return;
+				return
 			}
-			var recipients = _.pluck(shareModel.get('shares'), 'share_with_displayname');
+			var recipients = _.pluck(shareModel.get('shares'), 'share_with_displayname')
 			// note: we only update the data attribute because updateIcon()
 			if (recipients.length) {
-				var recipientData = _.mapObject(shareModel.get('shares'), function (share) {
-					return {shareWith: share.share_with, shareWithDisplayName: share.share_with_displayname};
-				});
-				$tr.attr('data-share-recipient-data', JSON.stringify(recipientData));
-			}
-			else {
-				$tr.removeAttr('data-share-recipient-data');
+				var recipientData = _.mapObject(shareModel.get('shares'), function(share) {
+					return { shareWith: share.share_with, shareWithDisplayName: share.share_with_displayname }
+				})
+				$tr.attr('data-share-recipient-data', JSON.stringify(recipientData))
+			} else {
+				$tr.removeAttr('data-share-recipient-data')
 			}
 		},
 
@@ -274,16 +273,16 @@
 		 * @param {boolean} hasUserShares true if a user share exists
 		 * @param {boolean} hasLinkShares true if a link share exists
 		 *
-		 * @return {boolean} true if the icon was set, false otherwise
+		 * @returns {boolean} true if the icon was set, false otherwise
 		 */
 		_updateFileActionIcon: function($tr, hasUserShares, hasLinkShares) {
 			// if the statuses are loaded already, use them for the icon
 			// (needed when scrolling to the next page)
 			if (hasUserShares || hasLinkShares || $tr.attr('data-share-recipient-data') || $tr.attr('data-share-owner')) {
-				OC.Share.markFileAsShared($tr, true, hasLinkShares);
-				return true;
+				OC.Share.markFileAsShared($tr, true, hasLinkShares)
+				return true
 			}
-			return false;
+			return false
 		},
 
 		/**
@@ -291,9 +290,9 @@
 		 * @returns {String}
 		 */
 		getSharePermissions: function(fileData) {
-			return fileData.sharePermissions;
+			return fileData.sharePermissions
 		}
-	};
-})();
+	}
+})()
 
-OC.Plugins.register('OCA.Files.FileList', OCA.Sharing.Util);
+OC.Plugins.register('OCA.Files.FileList', OCA.Sharing.Util)

+ 31 - 33
apps/files_sharing/src/sharebreadcrumbview.js

@@ -1,5 +1,3 @@
-/* global Handlebars, OC */
-
 /**
  * @copyright 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
  *
@@ -23,7 +21,7 @@
  */
 
 (function() {
-	'use strict';
+	'use strict'
 
 	var BreadCrumbView = OC.Backbone.View.extend({
 		tagName: 'span',
@@ -36,68 +34,68 @@
 		_shareTab: undefined,
 
 		initialize: function(options) {
-			this._shareTab = options.shareTab;
+			this._shareTab = options.shareTab
 		},
 
 		render: function(data) {
-			this._dirInfo = data.dirInfo || null;
+			this._dirInfo = data.dirInfo || null
 
 			if (this._dirInfo !== null && (this._dirInfo.path !== '/' || this._dirInfo.name !== '')) {
-				var isShared = data.dirInfo && data.dirInfo.shareTypes && data.dirInfo.shareTypes.length > 0;
-				this.$el.removeClass('shared icon-public icon-shared');
+				var isShared = data.dirInfo && data.dirInfo.shareTypes && data.dirInfo.shareTypes.length > 0
+				this.$el.removeClass('shared icon-public icon-shared')
 				if (isShared) {
-					this.$el.addClass('shared');
+					this.$el.addClass('shared')
 					if (data.dirInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_LINK) !== -1) {
-						this.$el.addClass('icon-public');
+						this.$el.addClass('icon-public')
 					} else {
-						this.$el.addClass('icon-shared');
+						this.$el.addClass('icon-shared')
 					}
 				} else {
-					this.$el.addClass('icon-shared');
+					this.$el.addClass('icon-shared')
 				}
-				this.$el.show();
-				this.delegateEvents();
+				this.$el.show()
+				this.delegateEvents()
 			} else {
-				this.$el.removeClass('shared icon-public icon-shared');
-				this.$el.hide();
+				this.$el.removeClass('shared icon-public icon-shared')
+				this.$el.hide()
 			}
 
-			return this;
+			return this
 		},
 		_onClick: function(e) {
-			e.preventDefault();
+			e.preventDefault()
 
-			var fileInfoModel = new OCA.Files.FileInfoModel(this._dirInfo);
-			var self = this;
+			var fileInfoModel = new OCA.Files.FileInfoModel(this._dirInfo)
+			var self = this
 			fileInfoModel.on('change', function() {
 				self.render({
 					dirInfo: self._dirInfo
-				});
-			});
+				})
+			})
 			this._shareTab.on('sharesChanged', function(shareModel) {
-				var shareTypes = [];
-				var shares = shareModel.getSharesWithCurrentItem();
+				var shareTypes = []
+				var shares = shareModel.getSharesWithCurrentItem()
 
-				for(var i = 0; i < shares.length; i++) {
+				for (var i = 0; i < shares.length; i++) {
 					if (shareTypes.indexOf(shares[i].share_type) === -1) {
-						shareTypes.push(shares[i].share_type);
+						shareTypes.push(shares[i].share_type)
 					}
 				}
 
 				if (shareModel.hasLinkShares()) {
-					shareTypes.push(OC.Share.SHARE_TYPE_LINK);
+					shareTypes.push(OC.Share.SHARE_TYPE_LINK)
 				}
 
 				// Since the dirInfo isn't updated we need to do this dark hackery
-				self._dirInfo.shareTypes = shareTypes;
+				self._dirInfo.shareTypes = shareTypes
 
 				self.render({
 					dirInfo: self._dirInfo
-				});
-			});
-			OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'shareTabView');
+				})
+			})
+			OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'shareTabView')
 		}
-	});
+	})
 
-	OCA.Sharing.ShareBreadCrumbView = BreadCrumbView;
-})();
+	OCA.Sharing.ShareBreadCrumbView = BreadCrumbView
+})()

+ 64 - 65
apps/files_sharing/src/sharetabview.js

@@ -11,77 +11,77 @@
 /* @global Handlebars */
 
 (function() {
-	var TEMPLATE =
-		'<div>' +
-		'<div class="dialogContainer"></div>' +
-		'<div id="collaborationResources"></div>' +
-		'</div>';
+	var TEMPLATE
+		= '<div>'
+		+ '<div class="dialogContainer"></div>'
+		+ '<div id="collaborationResources"></div>'
+		+ '</div>'
 
 	/**
 	 * @memberof OCA.Sharing
 	 */
 	var ShareTabView = OCA.Files.DetailTabView.extend(
 		/** @lends OCA.Sharing.ShareTabView.prototype */ {
-		id: 'shareTabView',
-		className: 'tab shareTabView',
+			id: 'shareTabView',
+			className: 'tab shareTabView',
 
-		initialize: function(name, options) {
-			OCA.Files.DetailTabView.prototype.initialize.call(this, name, options);
-			OC.Plugins.attach('OCA.Sharing.ShareTabView', this);
-		},
+			initialize: function(name, options) {
+				OCA.Files.DetailTabView.prototype.initialize.call(this, name, options)
+				OC.Plugins.attach('OCA.Sharing.ShareTabView', this)
+			},
 
-		template: function(params) {
-			return 	TEMPLATE;
-		},
+			template: function(params) {
+				return 	TEMPLATE
+			},
 
-		getLabel: function() {
-			return t('files_sharing', 'Sharing');
-		},
+			getLabel: function() {
+				return t('files_sharing', 'Sharing')
+			},
 
-		getIcon: function() {
-			return 'icon-shared';
-		},
+			getIcon: function() {
+				return 'icon-shared'
+			},
 
-		/**
+			/**
 		 * Renders this details view
 		 */
-		render: function() {
-			var self = this;
-			if (this._dialog) {
+			render: function() {
+				var self = this
+				if (this._dialog) {
 				// remove/destroy older instance
-				this._dialog.model.off();
-				this._dialog.remove();
-				this._dialog = null;
-			}
-
-			if (this.model) {
-				this.$el.html(this.template());
-
-				if (_.isUndefined(this.model.get('sharePermissions'))) {
-					this.model.set('sharePermissions', OCA.Sharing.Util.getSharePermissions(this.model.attributes));
+					this._dialog.model.off()
+					this._dialog.remove()
+					this._dialog = null
 				}
 
-				// TODO: the model should read these directly off the passed fileInfoModel
-				var attributes = {
-					itemType: this.model.isDirectory() ? 'folder' : 'file',
-				   	itemSource: this.model.get('id'),
-					possiblePermissions: this.model.get('sharePermissions')
-				};
-				var configModel = new OC.Share.ShareConfigModel();
-				var shareModel = new OC.Share.ShareItemModel(attributes, {
-					configModel: configModel,
-					fileInfoModel: this.model
-				});
-				this._dialog = new OC.Share.ShareDialogView({
-					configModel: configModel,
-					model: shareModel
-				});
-				this.$el.find('.dialogContainer').append(this._dialog.$el);
-				this._dialog.render();
-				this._dialog.model.fetch();
-				this._dialog.model.on('change', function() {
-					self.trigger('sharesChanged', shareModel);
-				});
+				if (this.model) {
+					this.$el.html(this.template())
+
+					if (_.isUndefined(this.model.get('sharePermissions'))) {
+						this.model.set('sharePermissions', OCA.Sharing.Util.getSharePermissions(this.model.attributes))
+					}
+
+					// TODO: the model should read these directly off the passed fileInfoModel
+					var attributes = {
+						itemType: this.model.isDirectory() ? 'folder' : 'file',
+						itemSource: this.model.get('id'),
+						possiblePermissions: this.model.get('sharePermissions')
+					}
+					var configModel = new OC.Share.ShareConfigModel()
+					var shareModel = new OC.Share.ShareItemModel(attributes, {
+						configModel: configModel,
+						fileInfoModel: this.model
+					})
+					this._dialog = new OC.Share.ShareDialogView({
+						configModel: configModel,
+						model: shareModel
+					})
+					this.$el.find('.dialogContainer').append(this._dialog.$el)
+					this._dialog.render()
+					this._dialog.model.fetch()
+					this._dialog.model.on('change', function() {
+						self.trigger('sharesChanged', shareModel)
+					})
 
 				import('./collaborationresources').then((Resources) => {
 					var vm = new Resources.Vue({
@@ -89,20 +89,19 @@
 						render: h => h(Resources.View),
 						data: {
 							model: this.model.toJSON()
-						},
-					});
+						}
+					})
 					this.model.on('change', () => { vm.data = this.model.toJSON() })
 
 				})
 
-			} else {
-				this.$el.empty();
+				} else {
+					this.$el.empty()
 				// TODO: render placeholder text?
+				}
+				this.trigger('rendered')
 			}
-			this.trigger('rendered');
-		}
-	});
-
-	OCA.Sharing.ShareTabView = ShareTabView;
-})();
+		})
 
+	OCA.Sharing.ShareTabView = ShareTabView
+})()

+ 11 - 8
apps/files_sharing/src/views/CollaborationView.vue

@@ -21,7 +21,10 @@
   -->
 
 <template>
-	<collection-list v-if="fileId" type="file" :id="fileId" :name="filename"></collection-list>
+	<CollectionList v-if="fileId"
+		:id="fileId"
+		type="file"
+		:name="filename" />
 </template>
 
 <script>
@@ -29,22 +32,22 @@ import { CollectionList } from 'nextcloud-vue-collections'
 
 export default {
 	name: 'CollaborationView',
+	components: {
+		CollectionList
+	},
 	computed: {
 		fileId() {
 			if (this.$root.model && this.$root.model.id) {
-				return '' + this.$root.model.id;
+				return '' + this.$root.model.id
 			}
-			return null;
+			return null
 		},
 		filename() {
 			if (this.$root.model && this.$root.model.name) {
-				return '' + this.$root.model.name;
+				return '' + this.$root.model.name
 			}
-			return '';
+			return ''
 		}
-	},
-	components: {
-		CollectionList
 	}
 }
 </script>

File diff suppressed because it is too large
+ 0 - 0
apps/files_trashbin/js/files_trashbin.js


File diff suppressed because it is too large
+ 0 - 0
apps/files_trashbin/js/files_trashbin.js.map


+ 53 - 54
apps/files_trashbin/src/app.js

@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright (c) 2014
  *
  * This file is licensed under the Affero General Public License version 3
@@ -11,7 +11,7 @@
 /**
  * @namespace OCA.Trashbin
  */
-OCA.Trashbin = {};
+OCA.Trashbin = {}
 /**
  * @namespace OCA.Trashbin.App
  */
@@ -20,19 +20,19 @@ OCA.Trashbin.App = {
 	/** @type {OC.Files.Client} */
 	client: null,
 
-	initialize: function ($el) {
+	initialize: function($el) {
 		if (this._initialized) {
-			return;
+			return
 		}
-		this._initialized = true;
+		this._initialized = true
 
 		this.client = new OC.Files.Client({
 			host: OC.getHost(),
 			port: OC.getPort(),
 			root: OC.linkToRemoteBase('dav') + '/trashbin/' + OC.getCurrentUser().uid,
 			useHTTPS: OC.getProtocol() === 'https'
-		});
-		var urlParams = OC.Util.History.parseUrlQuery();
+		})
+		var urlParams = OC.Util.History.parseUrlQuery()
 		this.fileList = new OCA.Trashbin.FileList(
 			$('#app-content-trashbin'), {
 				fileActions: this._createFileActions(),
@@ -43,12 +43,12 @@ OCA.Trashbin.App = {
 					{
 						name: 'restore',
 						displayName: t('files_trashbin', 'Restore'),
-						iconClass: 'icon-history',
+						iconClass: 'icon-history'
 					},
 					{
 						name: 'delete',
 						displayName: t('files_trashbin', 'Delete permanently'),
-						iconClass: 'icon-delete',
+						iconClass: 'icon-delete'
 					}
 				],
 				client: this.client,
@@ -57,18 +57,18 @@ OCA.Trashbin.App = {
 				// if handling the event with the file list already created.
 				shown: true
 			}
-		);
+		)
 	},
 
-	_createFileActions: function () {
-		var client = this.client;
-		var fileActions = new OCA.Files.FileActions();
-		fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
-			var dir = context.fileList.getCurrentDirectory();
-			context.fileList.changeDirectory(OC.joinPaths(dir, filename));
-		});
+	_createFileActions: function() {
+		var client = this.client
+		var fileActions = new OCA.Files.FileActions()
+		fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function(filename, context) {
+			var dir = context.fileList.getCurrentDirectory()
+			context.fileList.changeDirectory(OC.joinPaths(dir, filename))
+		})
 
-		fileActions.setDefault('dir', 'Open');
+		fileActions.setDefault('dir', 'Open')
 
 		fileActions.registerAction({
 			name: 'Restore',
@@ -77,21 +77,21 @@ OCA.Trashbin.App = {
 			mime: 'all',
 			permissions: OC.PERMISSION_READ,
 			iconClass: 'icon-history',
-			actionHandler: function (filename, context) {
-				var fileList = context.fileList;
-				var tr = fileList.findFileEl(filename);
-				fileList.showFileBusyState(tr, true);
-				var dir = context.fileList.getCurrentDirectory();
+			actionHandler: function(filename, context) {
+				var fileList = context.fileList
+				var tr = fileList.findFileEl(filename)
+				fileList.showFileBusyState(tr, true)
+				var dir = context.fileList.getCurrentDirectory()
 				client.move(OC.joinPaths('trash', dir, filename), OC.joinPaths('restore', filename), true)
 					.then(
 						fileList._removeCallback.bind(fileList, [filename]),
-						function () {
-							fileList.showFileBusyState(tr, false);
-							OC.Notification.show(t('files_trashbin', 'Error while restoring file from trashbin'));
+						function() {
+							fileList.showFileBusyState(tr, false)
+							OC.Notification.show(t('files_trashbin', 'Error while restoring file from trashbin'))
 						}
-					);
+					)
 			}
-		});
+		})
 
 		fileActions.registerAction({
 			name: 'Delete',
@@ -99,39 +99,38 @@ OCA.Trashbin.App = {
 			mime: 'all',
 			permissions: OC.PERMISSION_READ,
 			iconClass: 'icon-delete',
-			render: function (actionSpec, isDefault, context) {
-				var $actionLink = fileActions._makeActionLink(actionSpec, context);
-				$actionLink.attr('original-title', t('files_trashbin', 'Delete permanently'));
-				$actionLink.children('img').attr('alt', t('files_trashbin', 'Delete permanently'));
-				context.$file.find('td:last').append($actionLink);
-				return $actionLink;
+			render: function(actionSpec, isDefault, context) {
+				var $actionLink = fileActions._makeActionLink(actionSpec, context)
+				$actionLink.attr('original-title', t('files_trashbin', 'Delete permanently'))
+				$actionLink.children('img').attr('alt', t('files_trashbin', 'Delete permanently'))
+				context.$file.find('td:last').append($actionLink)
+				return $actionLink
 			},
-			actionHandler: function (filename, context) {
-				var fileList = context.fileList;
-				$('.tipsy').remove();
-				var tr = fileList.findFileEl(filename);
-				fileList.showFileBusyState(tr, true);
-				var dir = context.fileList.getCurrentDirectory();
+			actionHandler: function(filename, context) {
+				var fileList = context.fileList
+				$('.tipsy').remove()
+				var tr = fileList.findFileEl(filename)
+				fileList.showFileBusyState(tr, true)
+				var dir = context.fileList.getCurrentDirectory()
 				client.remove(OC.joinPaths('trash', dir, filename))
 					.then(
 						fileList._removeCallback.bind(fileList, [filename]),
-						function () {
-							fileList.showFileBusyState(tr, false);
-							OC.Notification.show(t('files_trashbin', 'Error while removing file from trashbin'));
+						function() {
+							fileList.showFileBusyState(tr, false)
+							OC.Notification.show(t('files_trashbin', 'Error while removing file from trashbin'))
 						}
-					);
+					)
 			}
-		});
-		return fileActions;
+		})
+		return fileActions
 	}
-};
+}
 
-$(document).ready(function () {
-	$('#app-content-trashbin').one('show', function () {
-		var App = OCA.Trashbin.App;
-		App.initialize($('#app-content-trashbin'));
+$(document).ready(function() {
+	$('#app-content-trashbin').one('show', function() {
+		var App = OCA.Trashbin.App
+		App.initialize($('#app-content-trashbin'))
 		// force breadcrumb init
 		// App.fileList.changeDirectory(App.fileList.getCurrentDirectory(), false, true);
-	});
-});
-
+	})
+})

+ 231 - 231
apps/files_trashbin/src/filelist.js

@@ -1,3 +1,4 @@
+/* eslint-disable */
 /*
  * Copyright (c) 2014
  *
@@ -8,25 +9,25 @@
  *
  */
 (function() {
-	var DELETED_REGEXP = new RegExp(/^(.+)\.d[0-9]+$/);
-	var FILENAME_PROP = '{http://nextcloud.org/ns}trashbin-filename';
-	var DELETION_TIME_PROP = '{http://nextcloud.org/ns}trashbin-deletion-time';
-	var TRASHBIN_ORIGINAL_LOCATION = '{http://nextcloud.org/ns}trashbin-original-location';
+	var DELETED_REGEXP = new RegExp(/^(.+)\.d[0-9]+$/)
+	var FILENAME_PROP = '{http://nextcloud.org/ns}trashbin-filename'
+	var DELETION_TIME_PROP = '{http://nextcloud.org/ns}trashbin-deletion-time'
+	var TRASHBIN_ORIGINAL_LOCATION = '{http://nextcloud.org/ns}trashbin-original-location'
 
 	/**
 	 * Convert a file name in the format filename.d12345 to the real file name.
 	 * This will use basename.
 	 * The name will not be changed if it has no ".d12345" suffix.
 	 * @param {String} name file name
-	 * @return {String} converted file name
+	 * @returns {String} converted file name
 	 */
 	function getDeletedFileName(name) {
-		name = OC.basename(name);
-		var match = DELETED_REGEXP.exec(name);
+		name = OC.basename(name)
+		var match = DELETED_REGEXP.exec(name)
 		if (match && match.length > 1) {
-			name = match[1];
+			name = match[1]
 		}
-		return name;
+		return name
 	}
 
 	/**
@@ -39,283 +40,282 @@
 	 * @param [options] map of options
 	 */
 	var FileList = function($el, options) {
-		this.client = options.client;
-		this.initialize($el, options);
-	};
+		this.client = options.client
+		this.initialize($el, options)
+	}
 	FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
 		/** @lends OCA.Trashbin.FileList.prototype */ {
-		id: 'trashbin',
-		appName: t('files_trashbin', 'Deleted files'),
-		/** @type {OC.Files.Client} */
-		client: null,
+			id: 'trashbin',
+			appName: t('files_trashbin', 'Deleted files'),
+			/** @type {OC.Files.Client} */
+			client: null,
 
-		/**
+			/**
 		 * @private
 		 */
-		initialize: function() {
-			this.client.addFileInfoParser(function(response, data) {
-				var props = response.propStat[0].properties;
-				var path = props[TRASHBIN_ORIGINAL_LOCATION];
-				return {
-					displayName: props[FILENAME_PROP],
-					mtime: parseInt(props[DELETION_TIME_PROP], 10) * 1000,
-					hasPreview: true,
-					path: path,
-					extraData: path
-				}
-			});
+			initialize: function() {
+				this.client.addFileInfoParser(function(response, data) {
+					var props = response.propStat[0].properties
+					var path = props[TRASHBIN_ORIGINAL_LOCATION]
+					return {
+						displayName: props[FILENAME_PROP],
+						mtime: parseInt(props[DELETION_TIME_PROP], 10) * 1000,
+						hasPreview: true,
+						path: path,
+						extraData: path
+					}
+				})
 
-			var result = OCA.Files.FileList.prototype.initialize.apply(this, arguments);
-			this.$el.find('.undelete').click('click', _.bind(this._onClickRestoreSelected, this));
+				var result = OCA.Files.FileList.prototype.initialize.apply(this, arguments)
+				this.$el.find('.undelete').click('click', _.bind(this._onClickRestoreSelected, this))
 
-			this.setSort('mtime', 'desc');
-			/**
+				this.setSort('mtime', 'desc')
+				/**
 			 * Override crumb making to add "Deleted Files" entry
 			 * and convert files with ".d" extensions to a more
 			 * user friendly name.
 			 */
-			this.breadcrumb._makeCrumbs = function() {
-				var parts = OCA.Files.BreadCrumb.prototype._makeCrumbs.apply(this, arguments);
-				for (var i = 1; i < parts.length; i++) {
-					parts[i].name = getDeletedFileName(parts[i].name);
+				this.breadcrumb._makeCrumbs = function() {
+					var parts = OCA.Files.BreadCrumb.prototype._makeCrumbs.apply(this, arguments)
+					for (var i = 1; i < parts.length; i++) {
+						parts[i].name = getDeletedFileName(parts[i].name)
+					}
+					return parts
 				}
-				return parts;
-			};
 
-			OC.Plugins.attach('OCA.Trashbin.FileList', this);
-			return result;
-		},
+				OC.Plugins.attach('OCA.Trashbin.FileList', this)
+				return result
+			},
 
-		/**
+			/**
 		 * Override to only return read permissions
 		 */
-		getDirectoryPermissions: function() {
-			return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
-		},
+			getDirectoryPermissions: function() {
+				return OC.PERMISSION_READ | OC.PERMISSION_DELETE
+			},
 
-		_setCurrentDir: function(targetDir) {
-			OCA.Files.FileList.prototype._setCurrentDir.apply(this, arguments);
+			_setCurrentDir: function(targetDir) {
+				OCA.Files.FileList.prototype._setCurrentDir.apply(this, arguments)
 
-			var baseDir = OC.basename(targetDir);
-			if (baseDir !== '') {
-				this.setPageTitle(getDeletedFileName(baseDir));
-			}
-		},
+				var baseDir = OC.basename(targetDir)
+				if (baseDir !== '') {
+					this.setPageTitle(getDeletedFileName(baseDir))
+				}
+			},
 
-		_createRow: function() {
+			_createRow: function() {
 			// FIXME: MEGAHACK until we find a better solution
-			var tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments);
-			tr.find('td.filesize').remove();
-			return tr;
-		},
-
-		getAjaxUrl: function(action, params) {
-			var q = '';
-			if (params) {
-				q = '?' + OC.buildQueryString(params);
-			}
-			return OC.filePath('files_trashbin', 'ajax', action + '.php') + q;
-		},
+				var tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments)
+				tr.find('td.filesize').remove()
+				return tr
+			},
+
+			getAjaxUrl: function(action, params) {
+				var q = ''
+				if (params) {
+					q = '?' + OC.buildQueryString(params)
+				}
+				return OC.filePath('files_trashbin', 'ajax', action + '.php') + q
+			},
 
-		setupUploadEvents: function() {
+			setupUploadEvents: function() {
 			// override and do nothing
-		},
+			},
 
-		linkTo: function(dir){
-			return OC.linkTo('files', 'index.php')+"?view=trashbin&dir="+ encodeURIComponent(dir).replace(/%2F/g, '/');
-		},
+			linkTo: function(dir) {
+				return OC.linkTo('files', 'index.php') + '?view=trashbin&dir=' + encodeURIComponent(dir).replace(/%2F/g, '/')
+			},
 
-		elementToFile: function($el) {
-			var fileInfo = OCA.Files.FileList.prototype.elementToFile($el);
-			if (this.getCurrentDirectory() === '/') {
-				fileInfo.displayName = getDeletedFileName(fileInfo.name);
-			}
-			// no size available
-			delete fileInfo.size;
-			return fileInfo;
-		},
-
-		updateEmptyContent: function(){
-			var exists = this.$fileList.find('tr:first').exists();
-			this.$el.find('#emptycontent').toggleClass('hidden', exists);
-			this.$el.find('#filestable th').toggleClass('hidden', !exists);
-		},
-
-		_removeCallback: function(files) {
-			var $el;
-			for (var i = 0; i < files.length; i++) {
-				$el = this.remove(OC.basename(files[i]), {updateSummary: false});
-				this.fileSummary.remove({type: $el.attr('data-type'), size: $el.attr('data-size')});
-			}
-			this.fileSummary.update();
-			this.updateEmptyContent();
-		},
-
-		_onClickRestoreSelected: function(event) {
-			event.preventDefault();
-			var self = this;
-			var files = _.pluck(this.getSelectedFiles(), 'name');
-			for (var i = 0; i < files.length; i++) {
-				var tr = this.findFileEl(files[i]);
-				this.showFileBusyState(tr, true);
-			}
-
-			this.fileMultiSelectMenu.toggleLoading('restore', true);
-			var restorePromises = files.map(function(file) {
-				return self.client.move(OC.joinPaths('trash', self.getCurrentDirectory(), file), OC.joinPaths('restore', file), true)
-					.then(
-						function() {
-							self._removeCallback([file]);
-						}
-					);
-			});
-			return Promise.all(restorePromises).then(
-				function() {
-					self.fileMultiSelectMenu.toggleLoading('restore', false);
-				},
-				function() {
-					OC.Notification.show(t('files_trashbin', 'Error while restoring files from trashbin'));
+			elementToFile: function($el) {
+				var fileInfo = OCA.Files.FileList.prototype.elementToFile($el)
+				if (this.getCurrentDirectory() === '/') {
+					fileInfo.displayName = getDeletedFileName(fileInfo.name)
+				}
+				// no size available
+				delete fileInfo.size
+				return fileInfo
+			},
+
+			updateEmptyContent: function() {
+				var exists = this.$fileList.find('tr:first').exists()
+				this.$el.find('#emptycontent').toggleClass('hidden', exists)
+				this.$el.find('#filestable th').toggleClass('hidden', !exists)
+			},
+
+			_removeCallback: function(files) {
+				var $el
+				for (var i = 0; i < files.length; i++) {
+					$el = this.remove(OC.basename(files[i]), { updateSummary: false })
+					this.fileSummary.remove({ type: $el.attr('data-type'), size: $el.attr('data-size') })
+				}
+				this.fileSummary.update()
+				this.updateEmptyContent()
+			},
+
+			_onClickRestoreSelected: function(event) {
+				event.preventDefault()
+				var self = this
+				var files = _.pluck(this.getSelectedFiles(), 'name')
+				for (var i = 0; i < files.length; i++) {
+					var tr = this.findFileEl(files[i])
+					this.showFileBusyState(tr, true)
 				}
-			);
-		},
-
-		_onClickDeleteSelected: function(event) {
-			event.preventDefault();
-			var self = this;
-			var allFiles = this.$el.find('.select-all').is(':checked');
-			var files = _.pluck(this.getSelectedFiles(), 'name');
-			for (var i = 0; i < files.length; i++) {
-				var tr = this.findFileEl(files[i]);
-				this.showFileBusyState(tr, true);
-			}
 
-			if (allFiles) {
-				return this.client.remove(OC.joinPaths('trash', this.getCurrentDirectory()))
-					.then(
-						function() {
-							self.hideMask();
-							self.setFiles([]);
-						},
-						function() {
-							OC.Notification.show(t('files_trashbin', 'Error while emptying trashbin'));
-						}
-					);
-			} else {
-				this.fileMultiSelectMenu.toggleLoading('delete', true);
-				var deletePromises = files.map(function(file) {
-					return self.client.remove(OC.joinPaths('trash', self.getCurrentDirectory(), file))
+				this.fileMultiSelectMenu.toggleLoading('restore', true)
+				var restorePromises = files.map(function(file) {
+					return self.client.move(OC.joinPaths('trash', self.getCurrentDirectory(), file), OC.joinPaths('restore', file), true)
 						.then(
 							function() {
-								self._removeCallback([file]);
+								self._removeCallback([file])
 							}
-						);
-				});
-				return Promise.all(deletePromises).then(
+						)
+				})
+				return Promise.all(restorePromises).then(
 					function() {
-						self.fileMultiSelectMenu.toggleLoading('delete', false);
+						self.fileMultiSelectMenu.toggleLoading('restore', false)
 					},
 					function() {
-						OC.Notification.show(t('files_trashbin', 'Error while removing files from trashbin'));
+						OC.Notification.show(t('files_trashbin', 'Error while restoring files from trashbin'))
 					}
-				);
-			}
-		},
+				)
+			},
+
+			_onClickDeleteSelected: function(event) {
+				event.preventDefault()
+				var self = this
+				var allFiles = this.$el.find('.select-all').is(':checked')
+				var files = _.pluck(this.getSelectedFiles(), 'name')
+				for (var i = 0; i < files.length; i++) {
+					var tr = this.findFileEl(files[i])
+					this.showFileBusyState(tr, true)
+				}
 
-		_onClickFile: function(event) {
-			var mime = $(this).parent().parent().data('mime');
-			if (mime !== 'httpd/unix-directory') {
-				event.preventDefault();
-			}
-			return OCA.Files.FileList.prototype._onClickFile.apply(this, arguments);
-		},
+				if (allFiles) {
+					return this.client.remove(OC.joinPaths('trash', this.getCurrentDirectory()))
+						.then(
+							function() {
+								self.hideMask()
+								self.setFiles([])
+							},
+							function() {
+								OC.Notification.show(t('files_trashbin', 'Error while emptying trashbin'))
+							}
+						)
+				} else {
+					this.fileMultiSelectMenu.toggleLoading('delete', true)
+					var deletePromises = files.map(function(file) {
+						return self.client.remove(OC.joinPaths('trash', self.getCurrentDirectory(), file))
+							.then(
+								function() {
+									self._removeCallback([file])
+								}
+							)
+					})
+					return Promise.all(deletePromises).then(
+						function() {
+							self.fileMultiSelectMenu.toggleLoading('delete', false)
+						},
+						function() {
+							OC.Notification.show(t('files_trashbin', 'Error while removing files from trashbin'))
+						}
+					)
+				}
+			},
+
+			_onClickFile: function(event) {
+				var mime = $(this).parent().parent().data('mime')
+				if (mime !== 'httpd/unix-directory') {
+					event.preventDefault()
+				}
+				return OCA.Files.FileList.prototype._onClickFile.apply(this, arguments)
+			},
 
-		generatePreviewUrl: function(urlSpec) {
-			return OC.generateUrl('/apps/files_trashbin/preview?') + $.param(urlSpec);
-		},
+			generatePreviewUrl: function(urlSpec) {
+				return OC.generateUrl('/apps/files_trashbin/preview?') + $.param(urlSpec)
+			},
 
-		getDownloadUrl: function() {
+			getDownloadUrl: function() {
 			// no downloads
-			return '#';
-		},
+				return '#'
+			},
 
-		updateStorageStatistics: function() {
+			updateStorageStatistics: function() {
 			// no op because the trashbin doesn't have
 			// storage info like free space / used space
-		},
+			},
 
-		isSelectedDeletable: function() {
-			return true;
-		},
+			isSelectedDeletable: function() {
+				return true
+			},
 
-		/**
+			/**
 		 * Returns list of webdav properties to request
 		 */
-		_getWebdavProperties: function() {
-			return [FILENAME_PROP, DELETION_TIME_PROP, TRASHBIN_ORIGINAL_LOCATION].concat(this.filesClient.getPropfindProperties());
-		},
+			_getWebdavProperties: function() {
+				return [FILENAME_PROP, DELETION_TIME_PROP, TRASHBIN_ORIGINAL_LOCATION].concat(this.filesClient.getPropfindProperties())
+			},
 
-		/**
+			/**
 		 * Reloads the file list using ajax call
 		 *
-		 * @return ajax call object
+		 * @returns ajax call object
 		 */
-		reload: function() {
-			this._selectedFiles = {};
-			this._selectionSummary.clear();
-			this.$el.find('.select-all').prop('checked', false);
-			this.showMask();
-			if (this._reloadCall) {
-				this._reloadCall.abort();
-			}
-			this._reloadCall = this.client.getFolderContents(
-				'trash/' + this.getCurrentDirectory(), {
-					includeParent: false,
-					properties: this._getWebdavProperties()
+			reload: function() {
+				this._selectedFiles = {}
+				this._selectionSummary.clear()
+				this.$el.find('.select-all').prop('checked', false)
+				this.showMask()
+				if (this._reloadCall) {
+					this._reloadCall.abort()
+				}
+				this._reloadCall = this.client.getFolderContents(
+					'trash/' + this.getCurrentDirectory(), {
+						includeParent: false,
+						properties: this._getWebdavProperties()
+					}
+				)
+				var callBack = this.reloadCallback.bind(this)
+				return this._reloadCall.then(callBack, callBack)
+			},
+			reloadCallback: function(status, result) {
+				delete this._reloadCall
+				this.hideMask()
+
+				if (status === 401) {
+					return false
 				}
-			);
-			var callBack = this.reloadCallback.bind(this);
-			return this._reloadCall.then(callBack, callBack);
-		},
-		reloadCallback: function(status, result) {
-			delete this._reloadCall;
-			this.hideMask();
-
-			if (status === 401) {
-				return false;
-			}
 
-			// Firewall Blocked request?
-			if (status === 403) {
+				// Firewall Blocked request?
+				if (status === 403) {
 				// Go home
-				this.changeDirectory('/');
-				OC.Notification.show(t('files', 'This operation is forbidden'));
-				return false;
-			}
+					this.changeDirectory('/')
+					OC.Notification.show(t('files', 'This operation is forbidden'))
+					return false
+				}
 
-			// Did share service die or something else fail?
-			if (status === 500) {
+				// Did share service die or something else fail?
+				if (status === 500) {
 				// Go home
-				this.changeDirectory('/');
-				OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'));
-				return false;
-			}
+					this.changeDirectory('/')
+					OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'))
+					return false
+				}
 
-			if (status === 404) {
+				if (status === 404) {
 				// go back home
-				this.changeDirectory('/');
-				return false;
-			}
-			// aborted ?
-			if (status === 0){
-				return true;
-			}
-
-			this.setFiles(result);
-			return true;
-		},
+					this.changeDirectory('/')
+					return false
+				}
+				// aborted ?
+				if (status === 0) {
+					return true
+				}
 
-	});
+				this.setFiles(result)
+				return true
+			}
 
-	OCA.Trashbin.FileList = FileList;
-})();
+		})
 
+	OCA.Trashbin.FileList = FileList
+})()

File diff suppressed because it is too large
+ 0 - 0
apps/files_versions/js/files_versions.js


File diff suppressed because it is too large
+ 0 - 0
apps/files_versions/js/files_versions.js.map


+ 6 - 7
apps/files_versions/src/filesplugin.js

@@ -9,7 +9,7 @@
  */
 
 (function() {
-	OCA.Versions = OCA.Versions || {};
+	OCA.Versions = OCA.Versions || {}
 
 	/**
 	 * @namespace
@@ -22,13 +22,12 @@
 		 */
 		attach: function(fileList) {
 			if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
-				return;
+				return
 			}
 
-			fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView', {order: -10}));
+			fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView', { order: -10 }))
 		}
-	};
-})();
-
-OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util);
+	}
+})()
 
+OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util)

+ 30 - 31
apps/files_versions/src/versioncollection.js

@@ -8,7 +8,7 @@
  *
  */
 
-(function () {
+(function() {
 	/**
 	 * @memberof OCA.Versions
 	 */
@@ -25,24 +25,24 @@
 
 		_client: null,
 
-		setFileInfo: function (fileInfo) {
-			this._fileInfo = fileInfo;
+		setFileInfo: function(fileInfo) {
+			this._fileInfo = fileInfo
 		},
 
-		getFileInfo: function () {
-			return this._fileInfo;
+		getFileInfo: function() {
+			return this._fileInfo
 		},
 
 		setCurrentUser: function(user) {
-			this._currentUser = user;
+			this._currentUser = user
 		},
 
 		getCurrentUser: function() {
-			return this._currentUser || OC.getCurrentUser().uid;
+			return this._currentUser || OC.getCurrentUser().uid
 		},
 
 		setClient: function(client) {
-			this._client = client;
+			this._client = client
 		},
 
 		getClient: function() {
@@ -50,35 +50,34 @@
 				host: OC.getHost(),
 				root: OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser(),
 				useHTTPS: OC.getProtocol() === 'https'
-			});
+			})
 		},
 
-		url: function () {
-			return OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser() + '/versions/' + this._fileInfo.get('id');
+		url: function() {
+			return OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser() + '/versions/' + this._fileInfo.get('id')
 		},
 
 		parse: function(result) {
-			var fullPath = this._fileInfo.getFullPath();
-			var fileId = this._fileInfo.get('id');
-			var name = this._fileInfo.get('name');
-			var user = this.getCurrentUser();
-			var client = this.getClient();
+			var fullPath = this._fileInfo.getFullPath()
+			var fileId = this._fileInfo.get('id')
+			var name = this._fileInfo.get('name')
+			var user = this.getCurrentUser()
+			var client = this.getClient()
 			return _.map(result, function(version) {
-				version.fullPath = fullPath;
-				version.fileId = fileId;
-				version.name = name;
-				version.timestamp = parseInt(moment(new Date(version.timestamp)).format('X'), 10);
-				version.id = OC.basename(version.href);
-				version.size = parseInt(version.size, 10);
-				version.user = user;
-				version.client = client;
-				return version;
-			});
+				version.fullPath = fullPath
+				version.fileId = fileId
+				version.name = name
+				version.timestamp = parseInt(moment(new Date(version.timestamp)).format('X'), 10)
+				version.id = OC.basename(version.href)
+				version.size = parseInt(version.size, 10)
+				version.user = user
+				version.client = client
+				return version
+			})
 		}
-	});
+	})
 
-	OCA.Versions = OCA.Versions || {};
-
-	OCA.Versions.VersionCollection = VersionCollection;
-})();
+	OCA.Versions = OCA.Versions || {}
 
+	OCA.Versions.VersionCollection = VersionCollection
+})()

+ 28 - 28
apps/files_versions/src/versionmodel.js

@@ -8,9 +8,7 @@
  *
  */
 
-/* global moment */
-
-(function () {
+(function() {
 	/**
 	 * @memberof OCA.Versions
 	 */
@@ -20,53 +18,55 @@
 		davProperties: {
 			'size': '{DAV:}getcontentlength',
 			'mimetype': '{DAV:}getcontenttype',
-			'timestamp': '{DAV:}getlastmodified',
+			'timestamp': '{DAV:}getlastmodified'
 		},
 
 		/**
 		 * Restores the original file to this revision
+		 *
+		 * @param {Object} [options] options
+		 * @returns {Promise}
 		 */
-		revert: function (options) {
-			options = options ? _.clone(options) : {};
-			var model = this;
+		revert: function(options) {
+			options = options ? _.clone(options) : {}
+			var model = this
 
-			var client = this.get('client');
+			var client = this.get('client')
 
 			return client.move('/versions/' + this.get('fileId') + '/' + this.get('id'), '/restore/target', true)
-				.done(function () {
+				.done(function() {
 					if (options.success) {
-						options.success.call(options.context, model, {}, options);
+						options.success.call(options.context, model, {}, options)
 					}
-					model.trigger('revert', model, options);
+					model.trigger('revert', model, options)
 				})
-				.fail(function () {
+				.fail(function() {
 					if (options.error) {
-						options.error.call(options.context, model, {}, options);
+						options.error.call(options.context, model, {}, options)
 					}
-					model.trigger('error', model, {}, options);
-				});
+					model.trigger('error', model, {}, options)
+				})
 		},
 
-		getFullPath: function () {
-			return this.get('fullPath');
+		getFullPath: function() {
+			return this.get('fullPath')
 		},
 
-		getPreviewUrl: function () {
-			var url = OC.generateUrl('/apps/files_versions/preview');
+		getPreviewUrl: function() {
+			var url = OC.generateUrl('/apps/files_versions/preview')
 			var params = {
 				file: this.get('fullPath'),
 				version: this.get('id')
-			};
-			return url + '?' + OC.buildQueryString(params);
+			}
+			return url + '?' + OC.buildQueryString(params)
 		},
 
-		getDownloadUrl: function () {
-			return OC.linkToRemoteBase('dav') + '/versions/' + this.get('user') + '/versions/' + this.get('fileId') + '/' + this.get('id');
+		getDownloadUrl: function() {
+			return OC.linkToRemoteBase('dav') + '/versions/' + this.get('user') + '/versions/' + this.get('fileId') + '/' + this.get('id')
 		}
-	});
-
-	OCA.Versions = OCA.Versions || {};
+	})
 
-	OCA.Versions.VersionModel = VersionModel;
-})();
+	OCA.Versions = OCA.Versions || {}
 
+	OCA.Versions.VersionModel = VersionModel
+})()

+ 76 - 76
apps/files_versions/src/versionstabview.js

@@ -8,7 +8,7 @@
  *
  */
 
-import ItemTemplate from './templates/item.handlebars';
+import ItemTemplate from './templates/item.handlebars'
 import Template from './templates/template.handlebars';
 
 (function() {
@@ -28,137 +28,137 @@ import Template from './templates/template.handlebars';
 		},
 
 		initialize: function() {
-			OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments);
-			this.collection = new OCA.Versions.VersionCollection();
-			this.collection.on('request', this._onRequest, this);
-			this.collection.on('sync', this._onEndRequest, this);
-			this.collection.on('update', this._onUpdate, this);
-			this.collection.on('error', this._onError, this);
-			this.collection.on('add', this._onAddModel, this);
+			OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments)
+			this.collection = new OCA.Versions.VersionCollection()
+			this.collection.on('request', this._onRequest, this)
+			this.collection.on('sync', this._onEndRequest, this)
+			this.collection.on('update', this._onUpdate, this)
+			this.collection.on('error', this._onError, this)
+			this.collection.on('add', this._onAddModel, this)
 		},
 
 		getLabel: function() {
-			return t('files_versions', 'Versions');
+			return t('files_versions', 'Versions')
 		},
 
 		getIcon: function() {
-			return 'icon-history';
+			return 'icon-history'
 		},
 
 		nextPage: function() {
 			if (this._loading) {
-				return;
+				return
 			}
 
 			if (this.collection.getFileInfo() && this.collection.getFileInfo().isDirectory()) {
-				return;
+				return
 			}
-			this.collection.fetch();
+			this.collection.fetch()
 		},
 
 		_onClickRevertVersion: function(ev) {
-			var self = this;
-			var $target = $(ev.target);
-			var fileInfoModel = this.collection.getFileInfo();
-			var revision;
+			var self = this
+			var $target = $(ev.target)
+			var fileInfoModel = this.collection.getFileInfo()
+			var revision
 			if (!$target.is('li')) {
-				$target = $target.closest('li');
+				$target = $target.closest('li')
 			}
 
-			ev.preventDefault();
-			revision = $target.attr('data-revision');
+			ev.preventDefault()
+			revision = $target.attr('data-revision')
 
-			var versionModel = this.collection.get(revision);
+			var versionModel = this.collection.get(revision)
 			versionModel.revert({
 				success: function() {
 					// reset and re-fetch the updated collection
-					self.$versionsContainer.empty();
-					self.collection.setFileInfo(fileInfoModel);
-					self.collection.reset([], {silent: true});
-					self.collection.fetch();
+					self.$versionsContainer.empty()
+					self.collection.setFileInfo(fileInfoModel)
+					self.collection.reset([], { silent: true })
+					self.collection.fetch()
 
-					self.$el.find('.versions').removeClass('hidden');
+					self.$el.find('.versions').removeClass('hidden')
 
 					// update original model
-					fileInfoModel.trigger('busy', fileInfoModel, false);
+					fileInfoModel.trigger('busy', fileInfoModel, false)
 					fileInfoModel.set({
 						size: versionModel.get('size'),
 						mtime: versionModel.get('timestamp') * 1000,
 						// temp dummy, until we can do a PROPFIND
 						etag: versionModel.get('id') + versionModel.get('timestamp')
-					});
+					})
 				},
 
 				error: function() {
-					fileInfoModel.trigger('busy', fileInfoModel, false);
-					self.$el.find('.versions').removeClass('hidden');
-					self._toggleLoading(false);
+					fileInfoModel.trigger('busy', fileInfoModel, false)
+					self.$el.find('.versions').removeClass('hidden')
+					self._toggleLoading(false)
 					OC.Notification.show(t('files_version', 'Failed to revert {file} to revision {timestamp}.',
 						{
 							file: versionModel.getFullPath(),
 							timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000)
 						}),
-						{
-							type: 'error'
-						}
-					);
+					{
+						type: 'error'
+					}
+					)
 				}
-			});
+			})
 
 			// spinner
-			this._toggleLoading(true);
-			fileInfoModel.trigger('busy', fileInfoModel, true);
+			this._toggleLoading(true)
+			fileInfoModel.trigger('busy', fileInfoModel, true)
 		},
 
 		_toggleLoading: function(state) {
-			this._loading = state;
-			this.$el.find('.loading').toggleClass('hidden', !state);
+			this._loading = state
+			this.$el.find('.loading').toggleClass('hidden', !state)
 		},
 
 		_onRequest: function() {
-			this._toggleLoading(true);
+			this._toggleLoading(true)
 		},
 
 		_onEndRequest: function() {
-			this._toggleLoading(false);
-			this.$el.find('.empty').toggleClass('hidden', !!this.collection.length);
+			this._toggleLoading(false)
+			this.$el.find('.empty').toggleClass('hidden', !!this.collection.length)
 		},
 
 		_onAddModel: function(model) {
-			var $el = $(this.itemTemplate(this._formatItem(model)));
-			this.$versionsContainer.append($el);
-			$el.find('.has-tooltip').tooltip();
+			var $el = $(this.itemTemplate(this._formatItem(model)))
+			this.$versionsContainer.append($el)
+			$el.find('.has-tooltip').tooltip()
 		},
 
 		template: function(data) {
-			return Template(data);
+			return Template(data)
 		},
 
 		itemTemplate: function(data) {
-			return ItemTemplate(data);
+			return ItemTemplate(data)
 		},
 
 		setFileInfo: function(fileInfo) {
 			if (fileInfo) {
-				this.render();
-				this.collection.setFileInfo(fileInfo);
-				this.collection.reset([], {silent: true});
-				this.nextPage();
+				this.render()
+				this.collection.setFileInfo(fileInfo)
+				this.collection.reset([], { silent: true })
+				this.nextPage()
 			} else {
-				this.render();
-				this.collection.reset();
+				this.render()
+				this.collection.reset()
 			}
 		},
 
 		_formatItem: function(version) {
-			var timestamp = version.get('timestamp') * 1000;
-			var size = version.has('size') ? version.get('size') : 0;
-			var preview = OC.MimeType.getIconUrl(version.get('mimetype'));
-			var img = new Image();
-			img.onload = function () {
-				$('li[data-revision=' + version.get('id') + '] .preview').attr('src', version.getPreviewUrl());
-			};
-			img.src = version.getPreviewUrl();
+			var timestamp = version.get('timestamp') * 1000
+			var size = version.has('size') ? version.get('size') : 0
+			var preview = OC.MimeType.getIconUrl(version.get('mimetype'))
+			var img = new Image()
+			img.onload = function() {
+				$('li[data-revision=' + version.get('id') + '] .preview').attr('src', version.getPreviewUrl())
+			}
+			img.src = version.getPreviewUrl()
 
 			return _.extend({
 				versionId: version.get('id'),
@@ -175,7 +175,7 @@ import Template from './templates/template.handlebars';
 				previewUrl: preview,
 				revertLabel: t('files_versions', 'Restore'),
 				canRevert: (this.collection.getFileInfo().get('permissions') & OC.PERMISSION_UPDATE) !== 0
-			}, version.attributes);
+			}, version.attributes)
 		},
 
 		/**
@@ -183,27 +183,27 @@ import Template from './templates/template.handlebars';
 		 */
 		render: function() {
 			this.$el.html(this.template({
-				emptyResultLabel: t('files_versions', 'No other versions available'),
-			}));
-			this.$el.find('.has-tooltip').tooltip();
-			this.$versionsContainer = this.$el.find('ul.versions');
-			this.delegateEvents();
+				emptyResultLabel: t('files_versions', 'No other versions available')
+			}))
+			this.$el.find('.has-tooltip').tooltip()
+			this.$versionsContainer = this.$el.find('ul.versions')
+			this.delegateEvents()
 		},
 
 		/**
 		 * Returns true for files, false for folders.
-		 *
-		 * @return {bool} true for files, false for folders
+		 * @param {FileInfo} fileInfo fileInfo
+		 * @returns {bool} true for files, false for folders
 		 */
 		canDisplay: function(fileInfo) {
 			if (!fileInfo) {
-				return false;
+				return false
 			}
-			return !fileInfo.isDirectory();
+			return !fileInfo.isDirectory()
 		}
-	});
+	})
 
-	OCA.Versions = OCA.Versions || {};
+	OCA.Versions = OCA.Versions || {}
 
-	OCA.Versions.VersionsTabView = VersionsTabView;
-})();
+	OCA.Versions.VersionsTabView = VersionsTabView
+})()

File diff suppressed because it is too large
+ 0 - 0
apps/oauth2/js/oauth2.js


File diff suppressed because it is too large
+ 0 - 0
apps/oauth2/js/oauth2.js.map


+ 50 - 31
apps/oauth2/src/App.vue

@@ -22,52 +22,71 @@
 <template>
 	<div id="oauth2" class="section">
 		<h2>{{ t('oauth2', 'OAuth 2.0 clients') }}</h2>
-		<p class="settings-hint">{{ t('oauth2', 'OAuth 2.0 allows external services to request access to {instanceName}.', { instanceName: OC.theme.name}) }}</p>
-		<table class="grid" v-if="clients.length > 0">
+		<p class="settings-hint">
+			{{ t('oauth2', 'OAuth 2.0 allows external services to request access to {instanceName}.', { instanceName: OC.theme.name}) }}
+		</p>
+		<table v-if="clients.length > 0" class="grid">
 			<thead>
 				<tr>
-					<th id="headerName" scope="col">{{ t('oauth2', 'Name') }}</th>
-					<th id="headerRedirectUri" scope="col">{{ t('oauth2', 'Redirection URI') }}</th>
-					<th id="headerClientIdentifier" scope="col">{{ t('oauth2', 'Client Identifier') }}</th>
-					<th id="headerSecret" scope="col">{{ t('oauth2', 'Secret') }}</th>
-					<th id="headerRemove">&nbsp;</th>
+					<th id="headerName" scope="col">
+						{{ t('oauth2', 'Name') }}
+					</th>
+					<th id="headerRedirectUri" scope="col">
+						{{ t('oauth2', 'Redirection URI') }}
+					</th>
+					<th id="headerClientIdentifier" scope="col">
+						{{ t('oauth2', 'Client Identifier') }}
+					</th>
+					<th id="headerSecret" scope="col">
+						{{ t('oauth2', 'Secret') }}
+					</th>
+					<th id="headerRemove">
+&nbsp;
+					</th>
 				</tr>
 			</thead>
 			<tbody>
 				<OAuthItem v-for="client in clients"
 					:key="client.id"
 					:client="client"
-					@delete="deleteClient"
-				/>
+					@delete="deleteClient" />
 			</tbody>
 		</table>
 
-		<br/>
+		<br>
 		<h3>{{ t('oauth2', 'Add client') }}</h3>
-		<span v-if="newClient.error" class="msg error">{{newClient.errorMsg}}</span>
+		<span v-if="newClient.error" class="msg error">{{ newClient.errorMsg }}</span>
 		<form @submit.prevent="addClient">
-			<input type="text" id="name" name="name" :placeholder="t('oauth2', 'Name')" v-model="newClient.name">
-			<input type="url" id="redirectUri" name="redirectUri" :placeholder="t('oauth2', 'Redirection URI')" v-model="newClient.redirectUri">
+			<input id="name"
+				v-model="newClient.name"
+				type="text"
+				name="name"
+				:placeholder="t('oauth2', 'Name')">
+			<input id="redirectUri"
+				v-model="newClient.redirectUri"
+				type="url"
+				name="redirectUri"
+				:placeholder="t('oauth2', 'Redirection URI')">
 			<input type="submit" class="button" :value="t('oauth2', 'Add')">
 		</form>
 	</div>
 </template>
 
 <script>
-import Axios from 'nextcloud-axios'
-import OAuthItem from './components/OAuthItem';
+import axios from 'nextcloud-axios'
+import OAuthItem from './components/OAuthItem'
 
 export default {
 	name: 'App',
+	components: {
+		OAuthItem
+	},
 	props: {
 		clients: {
 			type: Array,
 			required: true
 		}
 	},
-	components: {
-		OAuthItem
-	},
 	data: function() {
 		return {
 			newClient: {
@@ -76,34 +95,34 @@ export default {
 				errorMsg: '',
 				error: false
 			}
-		};
+		}
 	},
 	methods: {
 		deleteClient(id) {
-			Axios.delete(OC.generateUrl('apps/oauth2/clients/{id}', {id: id}))
+			axios.delete(OC.generateUrl('apps/oauth2/clients/{id}', { id: id }))
 				.then((response) => {
-					this.clients = this.clients.filter(client => client.id !== id);
-				});
+					this.clients = this.clients.filter(client => client.id !== id)
+				})
 		},
 		addClient() {
-			this.newClient.error = false;
+			this.newClient.error = false
 
-			Axios.post(
+			axios.post(
 				OC.generateUrl('apps/oauth2/clients'),
 				{
 					name: this.newClient.name,
 					redirectUri: this.newClient.redirectUri
 				}
 			).then(response => {
-				this.clients.push(response.data);
+				this.clients.push(response.data)
 
-				this.newClient.name = '';
-				this.newClient.redirectUri = '';
+				this.newClient.name = ''
+				this.newClient.redirectUri = ''
 			}).catch(reason => {
-				this.newClient.error = true;
-				this.newClient.errorMsg = reason.response.data.message;
-			});
+				this.newClient.error = true
+				this.newClient.errorMsg = reason.response.data.message
+			})
 		}
-	},
+	}
 }
 </script>

+ 18 - 16
apps/oauth2/src/components/OAuthItem.vue

@@ -21,11 +21,13 @@
   -->
 <template>
 	<tr>
-		<td>{{name}}</td>
-		<td>{{redirectUri}}</td>
-		<td><code>{{clientId}}</code></td>
-		<td><code>{{renderedSecret}}</code><a class='icon-toggle has-tooltip' :title="t('oauth2', 'Show client secret')" @click="toggleSecret"></a></td>
-		<td class="action-column"><span><a class="icon-delete has-tooltip" :title="t('oauth2', 'Delete')" @click="$emit('delete', id)"></a></span></td>
+		<td>{{ name }}</td>
+		<td>{{ redirectUri }}</td>
+		<td><code>{{ clientId }}</code></td>
+		<td><code>{{ renderedSecret }}</code><a class="icon-toggle has-tooltip" :title="t('oauth2', 'Show client secret')" @click="toggleSecret" /></td>
+		<td class="action-column">
+			<span><a class="icon-delete has-tooltip" :title="t('oauth2', 'Delete')" @click="$emit('delete', id)" /></span>
+		</td>
 	</tr>
 </template>
 
@@ -39,27 +41,27 @@ export default {
 		}
 	},
 	data: function() {
-			return {
-				id: this.client.id,
-				name: this.client.name,
-				redirectUri: this.client.redirectUri,
-				clientId: this.client.clientId,
-				clientSecret: this.client.clientSecret,
-				renderSecret: false,
-			};
+		return {
+			id: this.client.id,
+			name: this.client.name,
+			redirectUri: this.client.redirectUri,
+			clientId: this.client.clientId,
+			clientSecret: this.client.clientSecret,
+			renderSecret: false
+		}
 	},
 	computed: {
 		renderedSecret: function() {
 			if (this.renderSecret) {
-				return this.clientSecret;
+				return this.clientSecret
 			} else {
-				return '****';
+				return '****'
 			}
 		}
 	},
 	methods: {
 		toggleSecret() {
-			this.renderSecret = !this.renderSecret;
+			this.renderSecret = !this.renderSecret
 		}
 	}
 }

+ 8 - 7
apps/oauth2/src/main.js

@@ -20,18 +20,19 @@
  *
  */
 
-import Vue from 'vue';
-import App from './App.vue';
+import Vue from 'vue'
+import App from './App.vue'
 import { loadState } from 'nextcloud-initial-state'
 
-Vue.prototype.t = t;
-Vue.prototype.OC = OC;
+Vue.prototype.t = t
+Vue.prototype.OC = OC
 
-const clients = loadState('oauth2', 'clients');
+const clients = loadState('oauth2', 'clients')
 
 const View = Vue.extend(App)
-new View({
+const oauth = new View({
 	propsData: {
 		clients
 	}
-}).$mount('#oauth2');
+})
+oauth.$mount('#oauth2')

File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-0.js


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-0.js.map


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-4.js


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-4.js.map


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-5.js


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-5.js.map


File diff suppressed because it is too large
+ 1 - 1
apps/settings/js/vue-6.js


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-6.js.map


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-settings-admin-security.js


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-settings-admin-security.js.map


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-settings-apps-users-management.js


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-settings-apps-users-management.js.map


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-settings-personal-security.js


File diff suppressed because it is too large
+ 0 - 0
apps/settings/js/vue-settings-personal-security.js.map


+ 3 - 3
apps/settings/src/App.vue

@@ -21,7 +21,7 @@
   -->
 
 <template>
-	<router-view></router-view>
+	<router-view />
 </template>
 
 <script>
@@ -29,9 +29,9 @@ export default {
 	name: 'App',
 	beforeMount: function() {
 		// importing server data into the store
-		const serverDataElmt = document.getElementById('serverData');
+		const serverDataElmt = document.getElementById('serverData')
 		if (serverDataElmt !== null) {
-			this.$store.commit('setServerData', JSON.parse(document.getElementById('serverData').dataset.server));
+			this.$store.commit('setServerData', JSON.parse(document.getElementById('serverData').dataset.server))
 		}
 	}
 }

+ 103 - 106
apps/settings/src/components/AdminTwoFactor.vue

@@ -4,14 +4,14 @@
 			{{ t('settings', 'Two-factor authentication can be enforced for all	users and specific groups. If they do not have a two-factor provider configured, they will be unable to log into the system.') }}
 		</p>
 		<p v-if="loading">
-			<span class="icon-loading-small two-factor-loading"></span>
+			<span class="icon-loading-small two-factor-loading" />
 			<span>{{ t('settings', 'Enforce two-factor authentication') }}</span>
 		</p>
 		<p v-else>
-			<input type="checkbox"
-				   id="two-factor-enforced"
-				   class="checkbox"
-				   v-model="enforced">
+			<input id="two-factor-enforced"
+				v-model="enforced"
+				type="checkbox"
+				class="checkbox">
 			<label for="two-factor-enforced">{{ t('settings', 'Enforce two-factor authentication') }}</label>
 		</p>
 		<template v-if="enforced">
@@ -22,32 +22,30 @@
 			</p>
 			<p>
 				<Multiselect v-model="enforcedGroups"
-							 :options="groups"
-							 :placeholder="t('settings', 'Enforced groups')"
-							 :disabled="loading"
-							 :multiple="true"
-							 :searchable="true"
-							 @search-change="searchGroup"
-							 :loading="loadingGroups"
-							 :show-no-options="false"
-							 :close-on-select="false">
-				</Multiselect>
+					:options="groups"
+					:placeholder="t('settings', 'Enforced groups')"
+					:disabled="loading"
+					:multiple="true"
+					:searchable="true"
+					:loading="loadingGroups"
+					:show-no-options="false"
+					:close-on-select="false"
+					@search-change="searchGroup" />
 			</p>
 			<p>
 				{{ t('settings', 'Two-factor authentication is not enforced for	members of the following groups.') }}
 			</p>
 			<p>
 				<Multiselect v-model="excludedGroups"
-							 :options="groups"
-							 :placeholder="t('settings', 'Excluded groups')"
-							 :disabled="loading"
-							 :multiple="true"
-							 :searchable="true"
-							 @search-change="searchGroup"
-							 :loading="loadingGroups"
-							 :show-no-options="false"
-							 :close-on-select="false">
-				</Multiselect>
+					:options="groups"
+					:placeholder="t('settings', 'Excluded groups')"
+					:disabled="loading"
+					:multiple="true"
+					:searchable="true"
+					:loading="loadingGroups"
+					:show-no-options="false"
+					:close-on-select="false"
+					@search-change="searchGroup" />
 			</p>
 			<p>
 				<em>
@@ -57,10 +55,10 @@
 			</p>
 		</template>
 		<p>
-			<button class="button primary"
-					v-if="dirty"
-					v-on:click="saveChanges"
-					:disabled="loading">
+			<button v-if="dirty"
+				class="button primary"
+				:disabled="loading"
+				@click="saveChanges">
 				{{ t('settings', 'Save changes') }}
 			</button>
 		</p>
@@ -68,94 +66,93 @@
 </template>
 
 <script>
-	import Axios from 'nextcloud-axios'
-	import { mapState } from 'vuex'
-	import {Multiselect} from 'nextcloud-vue'
-	import _ from 'lodash'
+import axios from 'nextcloud-axios'
+import { Multiselect } from 'nextcloud-vue'
+import _ from 'lodash'
 
-	export default {
-		name: "AdminTwoFactor",
-		components: {
-			Multiselect
-		},
-		data () {
-			return {
-				loading: false,
-				dirty: false,
-				groups: [],
-				loadingGroups: false,
+export default {
+	name: 'AdminTwoFactor',
+	components: {
+		Multiselect
+	},
+	data() {
+		return {
+			loading: false,
+			dirty: false,
+			groups: [],
+			loadingGroups: false
+		}
+	},
+	computed: {
+		enforced: {
+			get: function() {
+				return this.$store.state.enforced
+			},
+			set: function(val) {
+				this.dirty = true
+				this.$store.commit('setEnforced', val)
 			}
 		},
-		computed: {
-			enforced: {
-				get: function () {
-					return this.$store.state.enforced
-				},
-				set: function (val) {
-					this.dirty = true
-					this.$store.commit('setEnforced', val)
-				}
-			},
-			enforcedGroups: {
-				get: function () {
-					return this.$store.state.enforcedGroups
-				},
-				set: function (val) {
-					this.dirty = true
-					this.$store.commit('setEnforcedGroups', val)
-				}
-			},
-			excludedGroups: {
-				get: function () {
-					return this.$store.state.excludedGroups
-				},
-				set: function (val) {
-					this.dirty = true
-					this.$store.commit('setExcludedGroups', val)
-				}
+		enforcedGroups: {
+			get: function() {
+				return this.$store.state.enforcedGroups
 			},
+			set: function(val) {
+				this.dirty = true
+				this.$store.commit('setEnforcedGroups', val)
+			}
 		},
-		mounted () {
-			// Groups are loaded dynamically, but the assigned ones *should*
-			// be valid groups, so let's add them as initial state
-			this.groups = _.sortedUniq(_.uniq(this.enforcedGroups.concat(this.excludedGroups)))
+		excludedGroups: {
+			get: function() {
+				return this.$store.state.excludedGroups
+			},
+			set: function(val) {
+				this.dirty = true
+				this.$store.commit('setExcludedGroups', val)
+			}
+		}
+	},
+	mounted() {
+		// Groups are loaded dynamically, but the assigned ones *should*
+		// be valid groups, so let's add them as initial state
+		this.groups = _.sortedUniq(_.uniq(this.enforcedGroups.concat(this.excludedGroups)))
 
-			// Populate the groups with a first set so the dropdown is not empty
-			// when opening the page the first time
-			this.searchGroup('')
-		},
-		methods: {
-			searchGroup: _.debounce(function (query) {
-				this.loadingGroups = true
-				Axios.get(OC.linkToOCS(`cloud/groups?offset=0&search=${encodeURIComponent(query)}&limit=20`, 2))
-					.then(res => res.data.ocs)
-					.then(ocs => ocs.data.groups)
-					.then(groups => this.groups = _.sortedUniq(_.uniq(this.groups.concat(groups))))
-					.catch(err => console.error('could not search groups', err))
-					.then(() => this.loadingGroups = false)
-			}, 500),
+		// Populate the groups with a first set so the dropdown is not empty
+		// when opening the page the first time
+		this.searchGroup('')
+	},
+	methods: {
+		searchGroup: _.debounce(function(query) {
+			this.loadingGroups = true
+			axios.get(OC.linkToOCS(`cloud/groups?offset=0&search=${encodeURIComponent(query)}&limit=20`, 2))
+				.then(res => res.data.ocs)
+				.then(ocs => ocs.data.groups)
+				.then(groups => { this.groups = _.sortedUniq(_.uniq(this.groups.concat(groups))) })
+				.catch(err => console.error('could not search groups', err))
+				.then(() => { this.loadingGroups = false })
+		}, 500),
 
-			saveChanges () {
-				this.loading = true
+		saveChanges() {
+			this.loading = true
 
-				const data = {
-					enforced: this.enforced,
-					enforcedGroups: this.enforcedGroups,
-					excludedGroups: this.excludedGroups,
-				}
-				Axios.put(OC.generateUrl('/settings/api/admin/twofactorauth'), data)
-					.then(resp => resp.data)
-					.then(state => {
-						this.state = state
-						this.dirty = false
-					})
-					.catch(err => {
-						console.error('could not save changes', err)
-					})
-					.then(() => this.loading = false)
+			const data = {
+				enforced: this.enforced,
+				enforcedGroups: this.enforcedGroups,
+				excludedGroups: this.excludedGroups
 			}
+			axios.put(OC.generateUrl('/settings/api/admin/twofactorauth'), data)
+				.then(resp => resp.data)
+				.then(state => {
+					this.state = state
+					this.dirty = false
+				})
+				.catch(err => {
+					console.error('could not save changes', err)
+				})
+				.then(() => { this.loading = false })
 		}
 	}
+}
 </script>
 
 <style>

+ 326 - 0
apps/settings/src/components/AppDetails.vue

@@ -0,0 +1,326 @@
+<!--
+  - @copyright Copyright (c) 2018 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>
+	<div id="app-details-view" style="padding: 20px;">
+		<h2>
+			<div v-if="!app.preview" class="icon-settings-dark" />
+			<svg v-if="app.previewAsIcon && app.preview"
+				width="32"
+				height="32"
+				viewBox="0 0 32 32">
+				<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter></defs>
+				<image x="0"
+					y="0"
+					width="32"
+					height="32"
+					preserveAspectRatio="xMinYMin meet"
+					:filter="filterUrl"
+					:xlink:href="app.preview"
+					class="app-icon" />
+			</svg>
+			{{ app.name }}
+		</h2>
+		<img v-if="app.screenshot" :src="app.screenshot" width="100%">
+		<div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level">
+			<span v-if="app.level === 300"
+				v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')"
+				class="supported icon-checkmark-color">
+				{{ t('settings', 'Supported') }}</span>
+			<span v-if="app.level === 200"
+				v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')"
+				class="official icon-checkmark">
+				{{ t('settings', 'Official') }}</span>
+			<AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" />
+		</div>
+
+		<div v-if="author" class="app-author">
+			{{ t('settings', 'by') }}
+			<span v-for="(a, index) in author" :key="index">
+				<a v-if="a['@attributes'] && a['@attributes']['homepage']" :href="a['@attributes']['homepage']">{{ a['@value'] }}</a><span v-else-if="a['@value']">{{ a['@value'] }}</span><span v-else>{{ a }}</span><span v-if="index+1 < author.length">, </span>
+			</span>
+		</div>
+		<div v-if="licence" class="app-licence">
+			{{ licence }}
+		</div>
+		<div class="actions">
+			<div class="actions-buttons">
+				<input v-if="app.update"
+					class="update primary"
+					type="button"
+					:value="t('settings', 'Update to {version}', {version: app.update})"
+					:disabled="installing || loading(app.id)"
+					@click="update(app.id)">
+				<input v-if="app.canUnInstall"
+					class="uninstall"
+					type="button"
+					:value="t('settings', 'Remove')"
+					:disabled="installing || loading(app.id)"
+					@click="remove(app.id)">
+				<input v-if="app.active"
+					class="enable"
+					type="button"
+					:value="t('settings','Disable')"
+					:disabled="installing || loading(app.id)"
+					@click="disable(app.id)">
+				<input v-if="!app.active && (app.canInstall || app.isCompatible)"
+					v-tooltip.auto="enableButtonTooltip"
+					class="enable primary"
+					type="button"
+					:value="enableButtonText"
+					:disabled="!app.canInstall || installing || loading(app.id)"
+					@click="enable(app.id)">
+				<input v-else-if="!app.active"
+					v-tooltip.auto="forceEnableButtonTooltip"
+					class="enable force"
+					type="button"
+					:value="forceEnableButtonText"
+					:disabled="installing || loading(app.id)"
+					@click="forceEnable(app.id)">
+			</div>
+			<div class="app-groups">
+				<div v-if="app.active && canLimitToGroups(app)" class="groups-enable">
+					<input :id="prefix('groups_enable', app.id)"
+						v-model="groupCheckedAppsData"
+						type="checkbox"
+						:value="app.id"
+						class="groups-enable__checkbox checkbox"
+						@change="setGroupLimit">
+					<label :for="prefix('groups_enable', app.id)">{{ t('settings', 'Limit to groups') }}</label>
+					<input type="hidden"
+						class="group_select"
+						:title="t('settings', 'All')"
+						value="">
+					<Multiselect v-if="isLimitedToGroups(app)"
+						:options="groups"
+						:value="appGroups"
+						:options-limit="5"
+						:placeholder="t('settings', 'Limit app usage to groups')"
+						label="name"
+						track-by="id"
+						class="multiselect-vue"
+						:multiple="true"
+						:close-on-select="false"
+						:tag-width="60"
+						@select="addGroupLimitation"
+						@remove="removeGroupLimitation"
+						@search-change="asyncFindGroup">
+						<span slot="noResult">{{ t('settings', 'No results') }}</span>
+					</Multiselect>
+				</div>
+			</div>
+		</div>
+
+		<ul class="app-dependencies">
+			<li v-if="app.missingMinOwnCloudVersion">
+				{{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}
+			</li>
+			<li v-if="app.missingMaxOwnCloudVersion">
+				{{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }}
+			</li>
+			<li v-if="!app.canInstall">
+				{{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
+				<ul class="missing-dependencies">
+					<li v-for="(dep, index) in app.missingDependencies" :key="index">
+						{{ dep }}
+					</li>
+				</ul>
+			</li>
+		</ul>
+
+		<p class="documentation">
+			<a v-if="!app.internal"
+				class="appslink"
+				:href="appstoreUrl"
+				target="_blank"
+				rel="noreferrer noopener">{{ t('settings', 'View in store') }} ↗</a>
+
+			<a v-if="app.website"
+				class="appslink"
+				:href="app.website"
+				target="_blank"
+				rel="noreferrer noopener">{{ t('settings', 'Visit website') }} ↗</a>
+			<a v-if="app.bugs"
+				class="appslink"
+				:href="app.bugs"
+				target="_blank"
+				rel="noreferrer noopener">{{ t('settings', 'Report a bug') }} ↗</a>
+
+			<a v-if="app.documentation && app.documentation.user"
+				class="appslink"
+				:href="app.documentation.user"
+				target="_blank"
+				rel="noreferrer noopener">{{ t('settings', 'User documentation') }} ↗</a>
+			<a v-if="app.documentation && app.documentation.admin"
+				class="appslink"
+				:href="app.documentation.admin"
+				target="_blank"
+				rel="noreferrer noopener">{{ t('settings', 'Admin documentation') }} ↗</a>
+			<a v-if="app.documentation && app.documentation.developer"
+				class="appslink"
+				:href="app.documentation.developer"
+				target="_blank"
+				rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} ↗</a>
+		</p>
+
+		<div class="app-description" v-html="renderMarkdown" />
+	</div>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue'
+import marked from 'marked'
+import dompurify from 'dompurify'
+
+import AppScore from './AppList/AppScore'
+import AppManagement from './AppManagement'
+import PrefixMixin from './PrefixMixin'
+import SvgFilterMixin from './SvgFilterMixin'
+
+export default {
+	name: 'AppDetails',
+	components: {
+		Multiselect,
+		AppScore
+	},
+	mixins: [AppManagement, PrefixMixin, SvgFilterMixin],
+	props: ['category', 'app'],
+	data() {
+		return {
+			groupCheckedAppsData: false
+		}
+	},
+	computed: {
+		appstoreUrl() {
+			return `https://apps.nextcloud.com/apps/${this.app.id}`
+		},
+		licence() {
+			if (this.app.licence) {
+				return t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() })
+			}
+			return null
+		},
+		hasRating() {
+			return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
+		},
+		author() {
+			if (typeof this.app.author === 'string') {
+				return [
+					{
+						'@value': this.app.author
+					}
+				]
+			}
+			if (this.app.author['@value']) {
+				return [this.app.author]
+			}
+			return this.app.author
+		},
+		appGroups() {
+			return this.app.groups.map(group => { return { id: group, name: group } })
+		},
+		groups() {
+			return this.$store.getters.getGroups
+				.filter(group => group.id !== 'disabled')
+				.sort((a, b) => a.name.localeCompare(b.name))
+		},
+		renderMarkdown() {
+			var renderer = new marked.Renderer()
+			renderer.link = function(href, title, text) {
+				try {
+					var prot = decodeURIComponent(unescape(href))
+						.replace(/[^\w:]/g, '')
+						.toLowerCase()
+				} catch (e) {
+					return ''
+				}
+
+				if (prot.indexOf('http:') !== 0 && prot.indexOf('https:') !== 0) {
+					return ''
+				}
+
+				var out = '<a href="' + href + '" rel="noreferrer noopener"'
+				if (title) {
+					out += ' title="' + title + '"'
+				}
+				out += '>' + text + '</a>'
+				return out
+			}
+			renderer.image = function(href, title, text) {
+				if (text) {
+					return text
+				}
+				return title
+			}
+			renderer.blockquote = function(quote) {
+				return quote
+			}
+			return dompurify.sanitize(
+				marked(this.app.description.trim(), {
+					renderer: renderer,
+					gfm: false,
+					highlight: false,
+					tables: false,
+					breaks: false,
+					pedantic: false,
+					sanitize: true,
+					smartLists: true,
+					smartypants: false
+				}),
+				{
+					SAFE_FOR_JQUERY: true,
+					ALLOWED_TAGS: [
+						'strong',
+						'p',
+						'a',
+						'ul',
+						'ol',
+						'li',
+						'em',
+						'del',
+						'blockquote'
+					]
+				}
+			)
+		}
+	},
+	mounted() {
+		if (this.app.groups.length > 0) {
+			this.groupCheckedAppsData = true
+		}
+	}
+}
+</script>
+
+<style scoped>
+	.force {
+		background: var(--color-main-background);
+		border-color: var(--color-error);
+		color: var(--color-error);
+	}
+	.force:hover,
+	.force:active {
+		background: var(--color-error);
+		border-color: var(--color-error) !important;
+		color: var(--color-main-background);
+	}
+</style>

+ 201 - 0
apps/settings/src/components/AppList.vue

@@ -0,0 +1,201 @@
+<!--
+  - @copyright Copyright (c) 2018 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>
+	<div id="app-content-inner">
+		<div id="apps-list" class="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
+			<template v-if="useListView">
+				<transition-group name="app-list" tag="div" class="apps-list-container">
+					<AppItem v-for="app in apps"
+						:key="app.id"
+						:app="app"
+						:category="category" />
+				</transition-group>
+			</template>
+			<transition-group v-if="useBundleView"
+				name="app-list"
+				tag="div"
+				class="apps-list-container">
+				<template v-for="bundle in bundles">
+					<div :key="bundle.id" class="apps-header">
+						<div class="app-image" />
+						<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" @click="toggleBundle(bundle.id)"></h2>
+						<div class="app-version" />
+						<div class="app-level" />
+						<div class="app-groups" />
+						<div class="actions">
+							&nbsp;
+						</div>
+					</div>
+					<AppItem v-for="app in bundleApps(bundle.id)"
+						:key="bundle.id + app.id"
+						:app="app"
+						:category="category" />
+				</template>
+			</transition-group>
+			<template v-if="useAppStoreView">
+				<AppItem v-for="app in apps"
+					:key="app.id"
+					:app="app"
+					:category="category"
+					:list-view="false" />
+			</template>
+		</div>
+
+		<div id="apps-list-search" class="apps-list installed">
+			<div class="apps-list-container">
+				<template v-if="search !== '' && searchApps.length > 0">
+					<div class="section">
+						<div />
+						<td colspan="5">
+							<h2>{{ t('settings', 'Results from other categories') }}</h2>
+						</td>
+					</div>
+					<AppItem v-for="app in searchApps"
+						:key="app.id"
+						:app="app"
+						:category="category"
+						:list-view="true" />
+				</template>
+			</div>
+		</div>
+
+		<div v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0" id="apps-list-empty" class="emptycontent emptycontent-search">
+			<div id="app-list-empty-icon" class="icon-settings-dark" />
+			<h2>{{ t('settings', 'No apps found for your version') }}</h2>
+		</div>
+
+		<div id="searchresults" />
+	</div>
+</template>
+
+<script>
+import AppItem from './AppList/AppItem'
+import PrefixMixin from './PrefixMixin'
+
+export default {
+	name: 'AppList',
+	components: {
+		AppItem
+	},
+	mixins: [PrefixMixin],
+	props: ['category', 'app', 'search'],
+	computed: {
+		loading() {
+			return this.$store.getters.loading('list')
+		},
+		apps() {
+			let apps = this.$store.getters.getAllApps
+				.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
+				.sort(function(a, b) {
+					const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name
+					const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name
+					return OC.Util.naturalSortCompare(sortStringA, sortStringB)
+				})
+
+			if (this.category === 'installed') {
+				return apps.filter(app => app.installed)
+			}
+			if (this.category === 'enabled') {
+				return apps.filter(app => app.active && app.installed)
+			}
+			if (this.category === 'disabled') {
+				return apps.filter(app => !app.active && app.installed)
+			}
+			if (this.category === 'app-bundles') {
+				return apps.filter(app => app.bundles)
+			}
+			if (this.category === 'updates') {
+				return apps.filter(app => app.update)
+			}
+			// filter app store categories
+			return apps.filter(app => {
+				return app.appstore && app.category !== undefined
+					&& (app.category === this.category || app.category.indexOf(this.category) > -1)
+			})
+		},
+		bundles() {
+			return this.$store.getters.getServerData.bundles.filter(bundle => this.bundleApps(bundle.id).length > 0)
+		},
+		bundleApps() {
+			return function(bundle) {
+				return this.$store.getters.getAllApps
+					.filter(app => app.bundleId === bundle)
+			}
+		},
+		searchApps() {
+			if (this.search === '') {
+				return []
+			}
+			return this.$store.getters.getAllApps
+				.filter(app => {
+					if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
+						return (!this.apps.find(_app => _app.id === app.id))
+					}
+					return false
+				})
+		},
+		useAppStoreView() {
+			return !this.useListView && !this.useBundleView
+		},
+		useListView() {
+			return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates')
+		},
+		useBundleView() {
+			return (this.category === 'app-bundles')
+		},
+		allBundlesEnabled() {
+			let self = this
+			return function(id) {
+				return self.bundleApps(id).filter(app => !app.active).length === 0
+			}
+		},
+		bundleToggleText() {
+			let self = this
+			return function(id) {
+				if (self.allBundlesEnabled(id)) {
+					return t('settings', 'Disable all')
+				}
+				return t('settings', 'Enable all')
+			}
+		}
+	},
+	methods: {
+		toggleBundle(id) {
+			if (this.allBundlesEnabled(id)) {
+				return this.disableBundle(id)
+			}
+			return this.enableBundle(id)
+		},
+		enableBundle(id) {
+			let apps = this.bundleApps(id).map(app => app.id)
+			this.$store.dispatch('enableApp', { appId: apps, groups: [] })
+				.catch((error) => { console.error(error); OC.Notification.show(error) })
+		},
+		disableBundle(id) {
+			let apps = this.bundleApps(id).map(app => app.id)
+			this.$store.dispatch('disableApp', { appId: apps, groups: [] })
+				.catch((error) => { OC.Notification.show(error) })
+		}
+	}
+}
+</script>

+ 179 - 0
apps/settings/src/components/AppList/AppItem.vue

@@ -0,0 +1,179 @@
+<!--
+  - @copyright Copyright (c) 2018 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>
+	<div class="section" :class="{ selected: isSelected }" @click="showAppDetails">
+		<div class="app-image app-image-icon" @click="showAppDetails">
+			<div v-if="(listView && !app.preview) || (!listView && !app.screenshot)" class="icon-settings-dark" />
+
+			<svg v-if="listView && app.preview"
+				width="32"
+				height="32"
+				viewBox="0 0 32 32">
+				<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter></defs>
+				<image x="0"
+					y="0"
+					width="32"
+					height="32"
+					preserveAspectRatio="xMinYMin meet"
+					:filter="filterUrl"
+					:xlink:href="app.preview"
+					class="app-icon" />
+			</svg>
+
+			<img v-if="!listView && app.screenshot" :src="app.screenshot" width="100%">
+		</div>
+		<div class="app-name" @click="showAppDetails">
+			{{ app.name }}
+		</div>
+		<div v-if="!listView" class="app-summary">
+			{{ app.summary }}
+		</div>
+		<div v-if="listView" class="app-version">
+			<span v-if="app.version">{{ app.version }}</span>
+			<span v-else-if="app.appstoreData.releases[0].version">{{ app.appstoreData.releases[0].version }}</span>
+		</div>
+
+		<div class="app-level">
+			<span v-if="app.level === 300"
+				v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')"
+				class="supported icon-checkmark-color">
+				{{ t('settings', 'Supported') }}</span>
+			<span v-if="app.level === 200"
+				v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')"
+				class="official icon-checkmark">
+				{{ t('settings', 'Official') }}</span>
+			<AppScore v-if="hasRating && !listView" :score="app.score" />
+		</div>
+
+		<div class="actions">
+			<div v-if="app.error" class="warning">
+				{{ app.error }}
+			</div>
+			<div v-if="loading(app.id)" class="icon icon-loading-small" />
+			<input v-if="app.update"
+				class="update primary"
+				type="button"
+				:value="t('settings', 'Update to {update}', {update:app.update})"
+				:disabled="installing || loading(app.id)"
+				@click.stop="update(app.id)">
+			<input v-if="app.canUnInstall"
+				class="uninstall"
+				type="button"
+				:value="t('settings', 'Remove')"
+				:disabled="installing || loading(app.id)"
+				@click.stop="remove(app.id)">
+			<input v-if="app.active"
+				class="enable"
+				type="button"
+				:value="t('settings','Disable')"
+				:disabled="installing || loading(app.id)"
+				@click.stop="disable(app.id)">
+			<input v-if="!app.active && (app.canInstall || app.isCompatible)"
+				v-tooltip.auto="enableButtonTooltip"
+				class="enable"
+				type="button"
+				:value="enableButtonText"
+				:disabled="!app.canInstall || installing || loading(app.id)"
+				@click.stop="enable(app.id)">
+			<input v-else-if="!app.active"
+				v-tooltip.auto="forceEnableButtonTooltip"
+				class="enable force"
+				type="button"
+				:value="forceEnableButtonText"
+				:disabled="installing || loading(app.id)"
+				@click.stop="forceEnable(app.id)">
+		</div>
+	</div>
+</template>
+
+<script>
+import AppScore from './AppScore'
+import AppManagement from '../AppManagement'
+import SvgFilterMixin from '../SvgFilterMixin'
+
+export default {
+	name: 'AppItem',
+	components: {
+		AppScore
+	},
+	mixins: [AppManagement, SvgFilterMixin],
+	props: {
+		app: {},
+		category: {},
+		listView: {
+			type: Boolean,
+			default: true
+		}
+	},
+	data() {
+		return {
+			isSelected: false,
+			scrolled: false
+		}
+	},
+	computed: {
+		hasRating() {
+			return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
+		}
+	},
+	watch: {
+		'$route.params.id': function(id) {
+			this.isSelected = (this.app.id === id)
+		}
+	},
+	mounted() {
+		this.isSelected = (this.app.id === this.$route.params.id)
+	},
+	watchers: {
+
+	},
+	methods: {
+		showAppDetails(event) {
+			if (event.currentTarget.tagName === 'INPUT' || event.currentTarget.tagName === 'A') {
+				return
+			}
+			this.$router.push({
+				name: 'apps-details',
+				params: { category: this.category, id: this.app.id }
+			})
+		},
+		prefix(prefix, content) {
+			return prefix + '_' + content
+		}
+	}
+}
+</script>
+
+<style scoped>
+	.force {
+		background: var(--color-main-background);
+		border-color: var(--color-error);
+		color: var(--color-error);
+	}
+	.force:hover,
+	.force:active {
+		background: var(--color-error);
+		border-color: var(--color-error) !important;
+		color: var(--color-main-background);
+	}
+</style>

+ 38 - 0
apps/settings/src/components/AppList/AppScore.vue

@@ -0,0 +1,38 @@
+<!--
+  - @copyright Copyright (c) 2018 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>
+	<img :src="scoreImage" class="app-score-image">
+</template>
+<script>
+export default {
+	name: 'AppScore',
+	props: ['score'],
+	computed: {
+		scoreImage() {
+			let score = Math.round(this.score * 10)
+			let imageName = 'rating/s' + score + '.svg'
+			return OC.imagePath('core', imageName)
+		}
+	}
+}
+</script>

+ 138 - 0
apps/settings/src/components/AppManagement.vue

@@ -0,0 +1,138 @@
+<!--
+  - @copyright Copyright (c) 2018 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/>.
+  -
+  -->
+
+<script>
+export default {
+	computed: {
+		appGroups() {
+			return this.app.groups.map(group => { return { id: group, name: group } })
+		},
+		loading() {
+			let self = this
+			return function(id) {
+				return self.$store.getters.loading(id)
+			}
+		},
+		installing() {
+			return this.$store.getters.loading('install')
+		},
+		enableButtonText() {
+			if (this.app.needsDownload) {
+				return t('settings', 'Download and enable')
+			}
+			return t('settings', 'Enable')
+		},
+		forceEnableButtonText() {
+			if (this.app.needsDownload) {
+				return t('settings', 'Enable untested app')
+			}
+			return t('settings', 'Enable untested app')
+		},
+		enableButtonTooltip() {
+			if (this.app.needsDownload) {
+				return t('settings', 'The app will be downloaded from the app store')
+			}
+			return false
+		},
+		forceEnableButtonTooltip() {
+			const base = t('settings', 'This app is not marked as compatible with your Nextcloud version. If you continue you will still be able to install the app. Note that the app might not work as expected.')
+			if (this.app.needsDownload) {
+				return base + ' ' + t('settings', 'The app will be downloaded from the app store')
+			}
+			return base
+		}
+	},
+	mounted() {
+		if (this.app.groups.length > 0) {
+			this.groupCheckedAppsData = true
+		}
+	},
+	methods: {
+		asyncFindGroup(query) {
+			return this.$store.dispatch('getGroups', { search: query, limit: 5, offset: 0 })
+		},
+		isLimitedToGroups(app) {
+			if (this.app.groups.length || this.groupCheckedAppsData) {
+				return true
+			}
+			return false
+		},
+		setGroupLimit: function() {
+			if (!this.groupCheckedAppsData) {
+				this.$store.dispatch('enableApp', { appId: this.app.id, groups: [] })
+			}
+		},
+		canLimitToGroups(app) {
+			if ((app.types && app.types.includes('filesystem'))
+					|| app.types.includes('prelogin')
+					|| app.types.includes('authentication')
+					|| app.types.includes('logging')
+					|| app.types.includes('prevent_group_restriction')) {
+				return false
+			}
+			return true
+		},
+		addGroupLimitation(group) {
+			let groups = this.app.groups.concat([]).concat([group.id])
+			this.$store.dispatch('enableApp', { appId: this.app.id, groups: groups })
+		},
+		removeGroupLimitation(group) {
+			let currentGroups = this.app.groups.concat([])
+			let index = currentGroups.indexOf(group.id)
+			if (index > -1) {
+				currentGroups.splice(index, 1)
+			}
+			this.$store.dispatch('enableApp', { appId: this.app.id, groups: currentGroups })
+		},
+		forceEnable(appId) {
+			this.$store.dispatch('forceEnableApp', { appId: appId, groups: [] })
+				.then((response) => { OC.Settings.Apps.rebuildNavigation() })
+				.catch((error) => { OC.Notification.show(error) })
+		},
+		enable(appId) {
+			this.$store.dispatch('enableApp', { appId: appId, groups: [] })
+				.then((response) => { OC.Settings.Apps.rebuildNavigation() })
+				.catch((error) => { OC.Notification.show(error) })
+		},
+		disable(appId) {
+			this.$store.dispatch('disableApp', { appId: appId })
+				.then((response) => { OC.Settings.Apps.rebuildNavigation() })
+				.catch((error) => { OC.Notification.show(error) })
+		},
+		remove(appId) {
+			this.$store.dispatch('uninstallApp', { appId: appId })
+				.then((response) => { OC.Settings.Apps.rebuildNavigation() })
+				.catch((error) => { OC.Notification.show(error) })
+		},
+		install(appId) {
+			this.$store.dispatch('enableApp', { appId: appId })
+				.then((response) => { OC.Settings.Apps.rebuildNavigation() })
+				.catch((error) => { OC.Notification.show(error) })
+		},
+		update(appId) {
+			this.$store.dispatch('updateApp', { appId: appId })
+				.then((response) => { OC.Settings.Apps.rebuildNavigation() })
+				.catch((error) => { OC.Notification.show(error) })
+		}
+	}
+}
+</script>

+ 64 - 64
apps/settings/src/components/AuthToken.vue

@@ -23,31 +23,29 @@
 	<tr :data-id="token.id"
 		:class="wiping">
 		<td class="client">
-			<div :class="iconName.icon"></div>
+			<div :class="iconName.icon" />
 		</td>
 		<td class="token-name">
 			<input v-if="token.canRename && renaming"
-				   type="text"
-				   ref="input"
-				   v-model="newName"
-				   @keyup.enter="rename"
-				   @blur="cancelRename"
-				   @keyup.esc="cancelRename">
-			<span v-else>{{iconName.name}}</span>
-			<span v-if="wiping"
-				  class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
+				ref="input"
+				v-model="newName"
+				type="text"
+				@keyup.enter="rename"
+				@blur="cancelRename"
+				@keyup.esc="cancelRename">
+			<span v-else>{{ iconName.name }}</span>
+			<span v-if="wiping" class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
 		</td>
 		<td>
-			<span class="last-activity" v-tooltip="lastActivity">{{lastActivityRelative}}</span>
+			<span v-tooltip="lastActivity" class="last-activity">{{ lastActivityRelative }}</span>
 		</td>
 		<td class="more">
 			<Actions v-if="!token.current"
-				:actions="actions"
-				:open.sync="actionOpen"
 				v-tooltip.auto="{
 					content: t('settings', 'Device settings'),
 					container: 'body'
-				}">
+				}"
+				:open.sync="actionOpen">
 				<ActionCheckbox v-if="token.type === 1"
 					:checked="token.scope.filesystem"
 					@change.stop.prevent="$emit('toggleScope', token, 'filesystem', !token.scope.filesystem)">
@@ -91,7 +89,7 @@ import {
 	Actions,
 	ActionButton,
 	ActionCheckbox
-} from 'nextcloud-vue';
+} from 'nextcloud-vue'
 
 const userAgentMap = {
 	ie: /(?:MSIE|Trident|Trident\/7.0; rv)[ :](\d+)/,
@@ -106,18 +104,18 @@ const userAgentMap = {
 	// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
 	androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/,
 	iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
-	ipad: /\(iPad\; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
-	iosClient: /^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/,
-	androidClient: /^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/,
-	iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud\-Talk.*$/,
-	androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud\-Talk.*$/,
+	ipad: /\(iPad; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
+	iosClient: /^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)-iOS.*$/,
+	androidClient: /^Mozilla\/5\.0 \(Android\) ownCloud-android.*$/,
+	iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud-Talk.*$/,
+	androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud-Talk.*$/,
 	// DAVdroid/1.2 (2016/07/03; dav4android; okhttp3) Android/6.0.1
 	davDroid: /DAV(droid|x5)\/([0-9.]+)/,
 	// Mozilla/5.0 (U; Linux; Maemo; Jolla; Sailfish; like Android 4.3) AppleWebKit/538.1 (KHTML, like Gecko) WebPirate/2.0 like Mobile Safari/538.1 (compatible)
 	webPirate: /(Sailfish).*WebPirate\/(\d+)/,
 	// Mozilla/5.0 (Maemo; Linux; U; Jolla; Sailfish; Mobile; rv:31.0) Gecko/31.0 Firefox/31.0 SailfishBrowser/1.0
 	sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/
-};
+}
 const nameMap = {
 	ie: t('setting', 'Internet Explorer'),
 	edge: t('setting', 'Edge'),
@@ -134,7 +132,7 @@ const nameMap = {
 	davDroid: 'DAVdroid',
 	webPirate: 'WebPirate',
 	sailfishBrowser: 'SailfishBrowser'
-};
+}
 const iconMap = {
 	ie: 'icon-desktop',
 	edge: 'icon-desktop',
@@ -151,10 +149,10 @@ const iconMap = {
 	davDroid: 'icon-phone',
 	webPirate: 'icon-link',
 	sailfishBrowser: 'icon-link'
-};
+}
 
 export default {
-	name: "AuthToken",
+	name: 'AuthToken',
 	components: {
 		Actions,
 		ActionButton,
@@ -163,91 +161,93 @@ export default {
 	props: {
 		token: {
 			type: Object,
-			required: true,
+			required: true
+		}
+	},
+	data() {
+		return {
+			showMore: this.token.canScope || this.token.canDelete,
+			renaming: false,
+			newName: '',
+			actionOpen: false
 		}
 	},
 	computed: {
-		lastActivityRelative () {
-			return OC.Util.relativeModifiedDate(this.token.lastActivity * 1000);
+		lastActivityRelative() {
+			return OC.Util.relativeModifiedDate(this.token.lastActivity * 1000)
 		},
-		lastActivity () {
-			return OC.Util.formatDate(this.token.lastActivity * 1000, 'LLL');
+		lastActivity() {
+			return OC.Util.formatDate(this.token.lastActivity * 1000, 'LLL')
 		},
-		iconName () {
+		iconName() {
 			// pretty format sync client user agent
-			let matches = this.token.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/);
+			let matches = this.token.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/)
 
-			let icon = '';
+			let icon = ''
 			if (matches) {
+				/* eslint-disable-next-line */
 				this.token.name = t('settings', 'Sync client - {os}', {
 					os: matches[1],
 					version: matches[2]
-				});
-				icon = 'icon-desktop';
+				})
+				icon = 'icon-desktop'
 			}
 
 			// preserve title for cases where we format it further
-			const title = this.token.name;
-			let name = this.token.name;
+			const title = this.token.name
+			let name = this.token.name
 			for (let client in userAgentMap) {
-				if (matches = title.match(userAgentMap[client])) {
+				const matches = title.match(userAgentMap[client])
+				if (matches) {
 					if (matches[2] && matches[1]) { // version number and os
-						name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1];
+						name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1]
 					} else if (matches[1]) { // only version number
-						name = nameMap[client] + ' ' + matches[1];
+						name = nameMap[client] + ' ' + matches[1]
 					} else {
-						name = nameMap[client];
+						name = nameMap[client]
 					}
 
-					icon = iconMap[client];
+					icon = iconMap[client]
 				}
 			}
 			if (this.token.current) {
-				name = t('settings', 'This session');
+				name = t('settings', 'This session')
 			}
 
 			return {
 				icon,
-				name,
-			};
+				name
+			}
 		},
 		wiping() {
-			return this.token.type === 2;
+			return this.token.type === 2
 		}
 	},
-	data () {
-		return {
-			showMore: this.token.canScope || this.token.canDelete,
-			renaming: false,
-			newName: '',
-			actionOpen: false,
-		};
-	},
 	methods: {
 		startRename() {
 			// Close action (popover menu)
-			this.actionOpen = false;
+			this.actionOpen = false
 
-			this.newName = this.token.name;
-			this.renaming = true;
+			this.newName = this.token.name
+			this.renaming = true
 			this.$nextTick(() => {
-				this.$refs.input.select();
-			});
+				this.$refs.input.select()
+			})
 		},
 		cancelRename() {
-			this.renaming = false;
+			this.renaming = false
 		},
 		revoke() {
-			this.actionOpen = false;
+			this.actionOpen = false
 			this.$emit('delete', this.token)
 		},
 		rename() {
-			this.renaming = false;
-			this.$emit('rename', this.token, this.newName);
+			this.renaming = false
+			this.$emit('rename', this.token, this.newName)
 		},
 		wipe() {
-			this.actionOpen = false;
-			this.$emit('wipe', this.token);
+			this.actionOpen = false
+			this.$emit('wipe', this.token)
 		}
 	}
 }

+ 48 - 48
apps/settings/src/components/AuthTokenList.vue

@@ -22,67 +22,67 @@
 <template>
 	<table id="app-tokens-table">
 		<thead v-if="tokens.length">
-		<tr>
-			<th></th>
-			<th>{{ t('settings', 'Device') }}</th>
-			<th>{{ t('settings', 'Last activity') }}</th>
-			<th></th>
-		</tr>
+			<tr>
+				<th />
+				<th>{{ t('settings', 'Device') }}</th>
+				<th>{{ t('settings', 'Last activity') }}</th>
+				<th />
+			</tr>
 		</thead>
 		<tbody class="token-list">
-		<AuthToken v-for="token in sortedTokens"
-				   :key="token.id"
-				   :token="token"
-				   @toggleScope="toggleScope"
-				   @rename="rename"
-				   @delete="onDelete"
-				   @wipe="onWipe" />
+			<AuthToken v-for="token in sortedTokens"
+				:key="token.id"
+				:token="token"
+				@toggleScope="toggleScope"
+				@rename="rename"
+				@delete="onDelete"
+				@wipe="onWipe" />
 		</tbody>
 	</table>
 </template>
 
 <script>
-	import AuthToken from './AuthToken';
+import AuthToken from './AuthToken'
 
-	export default {
-		name: 'AuthTokenList',
-		components: {
-			AuthToken
+export default {
+	name: 'AuthTokenList',
+	components: {
+		AuthToken
+	},
+	props: {
+		tokens: {
+			type: Array,
+			required: true
+		}
+	},
+	computed: {
+		sortedTokens() {
+			return this.tokens.slice().sort((t1, t2) => {
+				var ts1 = parseInt(t1.lastActivity, 10)
+				var ts2 = parseInt(t2.lastActivity, 10)
+				return ts2 - ts1
+			})
+		}
+	},
+	methods: {
+		toggleScope(token, scope, value) {
+			// Just pass it on
+			this.$emit('toggleScope', token, scope, value)
 		},
-		props: {
-			tokens: {
-				type: Array,
-				required: true,
-			}
+		rename(token, newName) {
+			// Just pass it on
+			this.$emit('rename', token, newName)
 		},
-		computed: {
-			sortedTokens () {
-				return this.tokens.sort((t1, t2) => {
-					var ts1 = parseInt(t1.lastActivity, 10);
-					var ts2 = parseInt(t2.lastActivity, 10);
-					return ts2 - ts1;
-				})
-			}
+		onDelete(token) {
+			// Just pass it on
+			this.$emit('delete', token)
 		},
-		methods: {
-			toggleScope (token, scope, value) {
-				// Just pass it on
-				this.$emit('toggleScope', token, scope, value);
-			},
-			rename (token, newName) {
-				// Just pass it on
-				this.$emit('rename', token, newName);
-			},
-			onDelete (token) {
-				// Just pass it on
-				this.$emit('delete', token);
-			},
-			onWipe(token) {
-				// Just pass it on
-				this.$emit('wipe', token);
-			}
+		onWipe(token) {
+			// Just pass it on
+			this.$emit('wipe', token)
 		}
 	}
+}
 </script>
 
 <style lang="scss" scoped>

+ 138 - 134
apps/settings/src/components/AuthTokenSection.vue

@@ -22,155 +22,159 @@
 <template>
 	<div id="security" class="section">
 		<h2>{{ t('settings', 'Devices & sessions') }}</h2>
-		<p class="settings-hint hidden-when-empty">{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }}</p>
+		<p class="settings-hint hidden-when-empty">
+			{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }}
+		</p>
 		<AuthTokenList :tokens="tokens"
-					   @toggleScope="toggleTokenScope"
-					   @rename="rename"
-					   @delete="deleteToken"
-					   @wipe="wipeToken" />
+			@toggleScope="toggleTokenScope"
+			@rename="rename"
+			@delete="deleteToken"
+			@wipe="wipeToken" />
 		<AuthTokenSetupDialogue v-if="canCreateToken" :add="addNewToken" />
 	</div>
 </template>
 
 <script>
-	import Axios from 'nextcloud-axios';
-	import confirmPassword from 'nextcloud-password-confirmation';
-
-	import AuthTokenList from './AuthTokenList';
-	import AuthTokenSetupDialogue from './AuthTokenSetupDialogue';
-
-	const confirm = () => {
-		return new Promise(res => {
-			OC.dialogs.confirm(
-				t('core', 'Do you really want to wipe your data from this device?'),
-				t('core', 'Confirm wipe'),
-				res,
-				true
-			)
-		})
-	}
+import axios from 'nextcloud-axios'
+import confirmPassword from 'nextcloud-password-confirmation'
+
+import AuthTokenList from './AuthTokenList'
+import AuthTokenSetupDialogue from './AuthTokenSetupDialogue'
+
+const confirm = () => {
+	return new Promise(resolve => {
+		OC.dialogs.confirm(
+			t('core', 'Do you really want to wipe your data from this device?'),
+			t('core', 'Confirm wipe'),
+			resolve,
+			true
+		)
+	})
+}
+
+/**
+ * Tap into a promise without losing the value
+ * @param {Function} cb the callback
+ * @returns {any} val the value
+ */
+const tap = cb => val => {
+	cb(val)
+	return val
+}
+
+export default {
+	name: 'AuthTokenSection',
+	components: {
+		AuthTokenSetupDialogue,
+		AuthTokenList
+	},
+	props: {
+		tokens: {
+			type: Array,
+			required: true
+		},
+		canCreateToken: {
+			type: Boolean,
+			required: true
+		}
+	},
+	data() {
+		return {
+			baseUrl: OC.generateUrl('/settings/personal/authtokens')
+		}
+	},
+	methods: {
+		addNewToken(name) {
+			console.debug('creating a new app token', name)
 
-	/**
-	 * Tap into a promise without losing the value
-	 */
-	const tap = cb => val => {
-		cb(val);
-		return val;
-	};
-
-	export default {
-		name: "AuthTokenSection",
-		props: {
-			tokens: {
-				type: Array,
-				required: true,
-			},
-			canCreateToken: {
-				type: Boolean,
-				required: true
+			const data = {
+				name
 			}
+			return axios.post(this.baseUrl, data)
+				.then(resp => resp.data)
+				.then(tap(() => console.debug('app token created')))
+				.then(tap(data => this.tokens.push(data.deviceToken)))
+				.catch(err => {
+					console.error.bind('could not create app password', err)
+					OC.Notification.showTemporary(t('core', 'Error while creating device token'))
+					throw err
+				})
 		},
-		components: {
-			AuthTokenSetupDialogue,
-			AuthTokenList
+		toggleTokenScope(token, scope, value) {
+			console.debug('updating app token scope', token.id, scope, value)
+
+			const oldVal = token.scope[scope]
+			token.scope[scope] = value
+
+			return this.updateToken(token)
+				.then(tap(() => console.debug('app token scope updated')))
+				.catch(err => {
+					console.error.bind('could not update app token scope', err)
+					OC.Notification.showTemporary(t('core', 'Error while updating device token scope'))
+
+					// Restore
+					token.scope[scope] = oldVal
+
+					throw err
+				})
 		},
-		data() {
-			return {
-				baseUrl: OC.generateUrl('/settings/personal/authtokens'),
-			}
+		rename(token, newName) {
+			console.debug('renaming app token', token.id, token.name, newName)
+
+			const oldName = token.name
+			token.name = newName
+
+			return this.updateToken(token)
+				.then(tap(() => console.debug('app token name updated')))
+				.catch(err => {
+					console.error.bind('could not update app token name', err)
+					OC.Notification.showTemporary(t('core', 'Error while updating device token name'))
+
+					// Restore
+					token.name = oldName
+				})
+		},
+		updateToken(token) {
+			return axios.put(this.baseUrl + '/' + token.id, token)
+				.then(resp => resp.data)
+		},
+		deleteToken(token) {
+			console.debug('deleting app token', token)
+
+			this.tokens = this.tokens.filter(t => t !== token)
+
+			return axios.delete(this.baseUrl + '/' + token.id)
+				.then(resp => resp.data)
+				.then(tap(() => console.debug('app token deleted')))
+				.catch(err => {
+					console.error.bind('could not delete app token', err)
+					OC.Notification.showTemporary(t('core', 'Error while deleting the token'))
+
+					// Restore
+					this.tokens.push(token)
+				})
 		},
-		methods: {
-			addNewToken (name) {
-				console.debug('creating a new app token', name);
-
-				const data = {
-					name,
-				};
-				return Axios.post(this.baseUrl, data)
-					.then(resp => resp.data)
-					.then(tap(() => console.debug('app token created')))
-					.then(tap(data => this.tokens.push(data.deviceToken)))
-					.catch(err => {
-						console.error.bind('could not create app password', err);
-						OC.Notification.showTemporary(t('core', 'Error while creating device token'));
-						throw err;
-					});
-			},
-			toggleTokenScope (token, scope, value) {
-				console.debug('updating app token scope', token.id, scope, value);
-
-				const oldVal = token.scope[scope];
-				token.scope[scope] = value;
-
-				return this.updateToken(token)
-					.then(tap(() => console.debug('app token scope updated')))
-					.catch(err => {
-						console.error.bind('could not update app token scope', err);
-						OC.Notification.showTemporary(t('core', 'Error while updating device token scope'));
-
-						// Restore
-						token.scope[scope] = oldVal;
-
-						throw err;
-					})
-			},
-			rename (token, newName) {
-				console.debug('renaming app token', token.id, token.name, newName);
-
-				const oldName = token.name;
-				token.name = newName;
-
-				return this.updateToken(token)
-					.then(tap(() => console.debug('app token name updated')))
-					.catch(err => {
-						console.error.bind('could not update app token name', err);
-						OC.Notification.showTemporary(t('core', 'Error while updating device token name'));
-
-						// Restore
-						token.name = oldName;
-					})
-			},
-			updateToken (token) {
-				return Axios.put(this.baseUrl + '/' + token.id, token)
-					.then(resp => resp.data)
-			},
-			deleteToken (token) {
-				console.debug('deleting app token', token);
-
-				this.tokens = this.tokens.filter(t => t !== token);
-
-				return Axios.delete(this.baseUrl + '/' + token.id)
-					.then(resp => resp.data)
-					.then(tap(() => console.debug('app token deleted')))
-					.catch(err => {
-						console.error.bind('could not delete app token', err);
-						OC.Notification.showTemporary(t('core', 'Error while deleting the token'));
-
-						// Restore
-						this.tokens.push(token);
-					})
-			},
-			async wipeToken(token) {
-				console.debug('wiping app token', token);
-
-				try {
-					await confirmPassword()
-
-					if (!(await confirm())) {
-						console.debug('wipe aborted by user')
-						return;
-					}
-					await Axios.post(this.baseUrl + '/wipe/' + token.id)
-					console.debug('app token marked for wipe')
-
-					token.type = 2;
-				} catch (err) {
-					console.error('could not wipe app token', err);
-					OC.Notification.showTemporary(t('core', 'Error while wiping the device with the token'));
+		async wipeToken(token) {
+			console.debug('wiping app token', token)
+
+			try {
+				await confirmPassword()
+
+				if (!(await confirm())) {
+					console.debug('wipe aborted by user')
+					return
 				}
+				await axios.post(this.baseUrl + '/wipe/' + token.id)
+				console.debug('app token marked for wipe')
+
+				token.type = 2
+			} catch (err) {
+				console.error('could not wipe app token', err)
+				OC.Notification.showTemporary(t('core', 'Error while wiping the device with the token'))
 			}
 		}
 	}
+}
 </script>
 
 <style scoped>

+ 114 - 113
apps/settings/src/components/AuthTokenSetupDialogue.vue

@@ -22,13 +22,14 @@
 <template>
 	<div v-if="!adding">
 		<input v-model="deviceName"
-			   type="text"
-			   @keydown.enter="submit"
-			   :disabled="loading"
-			   :placeholder="t('settings', 'App name')">
+			type="text"
+			:disabled="loading"
+			:placeholder="t('settings', 'App name')"
+			@keydown.enter="submit">
 		<button class="button"
-				:disabled="loading"
-				@click="submit">{{ t('settings', 'Create new app password')	}}
+			:disabled="loading"
+			@click="submit">
+			{{ t('settings', 'Create new app password')	}}
 		</button>
 	</div>
 	<div v-else>
@@ -37,142 +38,142 @@
 		<div class="app-password-row">
 			<span class="app-password-label">{{ t('settings', 'Username') }}</span>
 			<input :value="loginName"
-				   type="text"
-				   class="monospaced"
-				   readonly="readonly"
-				   @focus="selectInput"/>
+				type="text"
+				class="monospaced"
+				readonly="readonly"
+				@focus="selectInput">
 		</div>
 		<div class="app-password-row">
 			<span class="app-password-label">{{ t('settings', 'Password') }}</span>
-			<input :value="appPassword"
-				   type="text"
-				   class="monospaced"
-				   ref="appPassword"
-				   readonly="readonly"
-				   @focus="selectInput"/>
-			<a class="icon icon-clippy"
-			   ref="clipboardButton"
-			   v-tooltip="copyTooltipOptions"
-			   @mouseover="hoveringCopyButton = true"
-			   @mouseleave="hoveringCopyButton = false"
-			   v-clipboard:copy="appPassword"
-			   v-clipboard:success="onCopyPassword"
-			   v-clipboard:error="onCopyPasswordFailed"></a>
+			<input ref="appPassword"
+				:value="appPassword"
+				type="text"
+				class="monospaced"
+				readonly="readonly"
+				@focus="selectInput">
+			<a ref="clipboardButton"
+				v-tooltip="copyTooltipOptions"
+				v-clipboard:copy="appPassword"
+				v-clipboard:success="onCopyPassword"
+				v-clipboard:error="onCopyPasswordFailed"
+				class="icon icon-clippy"
+				@mouseover="hoveringCopyButton = true"
+				@mouseleave="hoveringCopyButton = false" />
 			<button class="button"
-					@click="reset">
+				@click="reset">
 				{{ t('settings', 'Done') }}
 			</button>
 		</div>
 		<div class="app-password-row">
-			<span class="app-password-label"></span>
+			<span class="app-password-label" />
 			<a v-if="!showQR"
-			   @click="showQR = true">
+				@click="showQR = true">
 				{{ t('settings', 'Show QR code for mobile apps') }}
 			</a>
 			<QR v-else
-				:value="qrUrl"></QR>
+				:value="qrUrl" />
 		</div>
 	</div>
 </template>
 
 <script>
-	import QR from '@chenfengyuan/vue-qrcode';
-	import confirmPassword from 'nextcloud-password-confirmation';
+import QR from '@chenfengyuan/vue-qrcode'
+import confirmPassword from 'nextcloud-password-confirmation'
 
-	export default {
-		name: 'AuthTokenSetupDialogue',
-		components: {
-			QR,
-		},
-		props: {
-			add: {
-				type: Function,
-				required: true,
-			}
-		},
-		data () {
-			return {
-				adding: false,
-				loading: false,
-				deviceName: '',
-				appPassword: '',
-				loginName: '',
-				passwordCopied: false,
-				showQR: false,
-				qrUrl: '',
-				hoveringCopyButton: false,
+export default {
+	name: 'AuthTokenSetupDialogue',
+	components: {
+		QR
+	},
+	props: {
+		add: {
+			type: Function,
+			required: true
+		}
+	},
+	data() {
+		return {
+			adding: false,
+			loading: false,
+			deviceName: '',
+			appPassword: '',
+			loginName: '',
+			passwordCopied: false,
+			showQR: false,
+			qrUrl: '',
+			hoveringCopyButton: false
+		}
+	},
+	computed: {
+		copyTooltipOptions() {
+			const base = {
+				hideOnTargetClick: false,
+				trigger: 'manual'
 			}
-		},
-		computed: {
-			copyTooltipOptions() {
-				const base = {
-					hideOnTargetClick: false,
-					trigger: 'manual',
-				};
 
-				if (this.passwordCopied) {
-					return {
-						...base,
-						content:t('core', 'Copied!'),
-						show: true,
-					}
-				} else {
-					return {
-						...base,
-						content: t('core', 'Copy'),
-						show: this.hoveringCopyButton,
-					}
+			if (this.passwordCopied) {
+				return {
+					...base,
+					content: t('core', 'Copied!'),
+					show: true
+				}
+			} else {
+				return {
+					...base,
+					content: t('core', 'Copy'),
+					show: this.hoveringCopyButton
 				}
 			}
+		}
+	},
+	methods: {
+		selectInput(e) {
+			e.currentTarget.select()
 		},
-		methods: {
-			selectInput (e) {
-				e.currentTarget.select();
-			},
-			submit: function () {
-				confirmPassword()
-					.then(() => {
-						this.loading = true;
-						return this.add(this.deviceName)
-					})
-					.then(token => {
-						this.adding = true;
-						this.loginName = token.loginName;
-						this.appPassword = token.token;
+		submit: function() {
+			confirmPassword()
+				.then(() => {
+					this.loading = true
+					return this.add(this.deviceName)
+				})
+				.then(token => {
+					this.adding = true
+					this.loginName = token.loginName
+					this.appPassword = token.token
 
-						const server = window.location.protocol + '//' + window.location.host + OC.getRootPath();
-						this.qrUrl = `nc://login/user:${token.loginName}&password:${token.token}&server:${server}`;
+					const server = window.location.protocol + '//' + window.location.host + OC.getRootPath()
+					this.qrUrl = `nc://login/user:${token.loginName}&password:${token.token}&server:${server}`
 
-						this.$nextTick(() => {
-							this.$refs.appPassword.select();
-						});
+					this.$nextTick(() => {
+						this.$refs.appPassword.select()
 					})
-					.catch(err => {
-						console.error('could not create a new app password', err);
-						OC.Notification.showTemporary(t('core', 'Error while creating device token'));
+				})
+				.catch(err => {
+					console.error('could not create a new app password', err)
+					OC.Notification.showTemporary(t('core', 'Error while creating device token'))
 
-						this.reset();
-					});
-			},
-			onCopyPassword() {
-				this.passwordCopied = true;
-				this.$refs.clipboardButton.blur();
-				setTimeout(() => this.passwordCopied = false, 3000);
-			},
-			onCopyPasswordFailed() {
-				OC.Notification.showTemporary(t('core', 'Could not copy app password. Please copy it manually.'));
-			},
-			reset () {
-				this.adding = false;
-				this.loading = false;
-				this.showQR = false;
-				this.qrUrl = '';
-				this.deviceName = '';
-				this.appPassword = '';
-				this.loginName = '';
-			}
+					this.reset()
+				})
+		},
+		onCopyPassword() {
+			this.passwordCopied = true
+			this.$refs.clipboardButton.blur()
+			setTimeout(() => { this.passwordCopied = false }, 3000)
+		},
+		onCopyPasswordFailed() {
+			OC.Notification.showTemporary(t('core', 'Could not copy app password. Please copy it manually.'))
+		},
+		reset() {
+			this.adding = false
+			this.loading = false
+			this.showQR = false
+			this.qrUrl = ''
+			this.deviceName = ''
+			this.appPassword = ''
+			this.loginName = ''
 		}
 	}
+}
 </script>
 
 <style lang="scss" scoped>

+ 32 - 0
apps/settings/src/components/PrefixMixin.vue

@@ -0,0 +1,32 @@
+<!--
+  - @copyright Copyright (c) 2018 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/>.
+  -
+  -->
+
+<script>
+export default {
+	name: 'PrefixMixin',
+	methods: {
+		prefix(prefix, content) {
+			return prefix + '_' + content
+		}
+	}
+}
+</script>

+ 40 - 0
apps/settings/src/components/SvgFilterMixin.vue

@@ -0,0 +1,40 @@
+<!--
+  - @copyright Copyright (c) 2018 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/>.
+  -
+  -->
+
+<script>
+export default {
+	name: 'SvgFilterMixin',
+	data() {
+		return {
+			filterId: ''
+		}
+	},
+	computed: {
+		filterUrl() {
+			return `url(#${this.filterId})`
+		}
+	},
+	mounted() {
+		this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100)) + new Date().getSeconds() + new Date().getMilliseconds()
+	}
+}
+</script>

+ 553 - 0
apps/settings/src/components/UserList.vue

@@ -0,0 +1,553 @@
+<!--
+  - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @author John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @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>
+	<div id="app-content" class="user-list-grid" @scroll.passive="onScroll">
+		<div id="grid-header" class="row" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
+			<div id="headerAvatar" class="avatar" />
+			<div id="headerName" class="name">
+				{{ t('settings', 'Username') }}
+			</div>
+			<div id="headerDisplayName" class="displayName">
+				{{ t('settings', 'Display name') }}
+			</div>
+			<div id="headerPassword" class="password">
+				{{ t('settings', 'Password') }}
+			</div>
+			<div id="headerAddress" class="mailAddress">
+				{{ t('settings', 'Email') }}
+			</div>
+			<div id="headerGroups" class="groups">
+				{{ t('settings', 'Groups') }}
+			</div>
+			<div v-if="subAdminsGroups.length>0 && settings.isAdmin"
+				id="headerSubAdmins"
+				class="subadmins">
+				{{ t('settings', 'Group admin for') }}
+			</div>
+			<div id="headerQuota" class="quota">
+				{{ t('settings', 'Quota') }}
+			</div>
+			<div v-if="showConfig.showLanguages"
+				id="headerLanguages"
+				class="languages">
+				{{ t('settings', 'Language') }}
+			</div>
+			<div v-if="showConfig.showStoragePath"
+				class="headerStorageLocation storageLocation">
+				{{ t('settings', 'Storage location') }}
+			</div>
+			<div v-if="showConfig.showUserBackend"
+				class="headerUserBackend userBackend">
+				{{ t('settings', 'User backend') }}
+			</div>
+			<div v-if="showConfig.showLastLogin"
+				class="headerLastLogin lastLogin">
+				{{ t('settings', 'Last login') }}
+			</div>
+			<div class="userActions" />
+		</div>
+
+		<form v-show="showConfig.showNewUserForm"
+			id="new-user"
+			class="row"
+			:disabled="loading.all"
+			:class="{'sticky': scrolled && showConfig.showNewUserForm}"
+			@submit.prevent="createUser">
+			<div :class="loading.all?'icon-loading-small':'icon-add'" />
+			<div class="name">
+				<input id="newusername"
+					ref="newusername"
+					v-model="newUser.id"
+					type="text"
+					required
+					:placeholder="settings.newUserGenerateUserID
+						? t('settings', 'Will be autogenerated')
+						: t('settings', 'Username')"
+					name="username"
+					autocomplete="off"
+					autocapitalize="none"
+					autocorrect="off"
+					pattern="[a-zA-Z0-9 _\.@\-']+"
+					:disabled="settings.newUserGenerateUserID">
+			</div>
+			<div class="displayName">
+				<input id="newdisplayname"
+					v-model="newUser.displayName"
+					type="text"
+					:placeholder="t('settings', 'Display name')"
+					name="displayname"
+					autocomplete="off"
+					autocapitalize="none"
+					autocorrect="off">
+			</div>
+			<div class="password">
+				<input id="newuserpassword"
+					ref="newuserpassword"
+					v-model="newUser.password"
+					type="password"
+					:required="newUser.mailAddress===''"
+					:placeholder="t('settings', 'Password')"
+					name="password"
+					autocomplete="new-password"
+					autocapitalize="none"
+					autocorrect="off"
+					:minlength="minPasswordLength">
+			</div>
+			<div class="mailAddress">
+				<input id="newemail"
+					v-model="newUser.mailAddress"
+					type="email"
+					:required="newUser.password==='' || settings.newUserRequireEmail"
+					:placeholder="t('settings', 'Email')"
+					name="email"
+					autocomplete="off"
+					autocapitalize="none"
+					autocorrect="off">
+			</div>
+			<div class="groups">
+				<!-- hidden input trick for vanilla html5 form validation -->
+				<input v-if="!settings.isAdmin"
+					id="newgroups"
+					type="text"
+					:value="newUser.groups"
+					tabindex="-1"
+					:required="!settings.isAdmin"
+					:class="{'icon-loading-small': loading.groups}">
+				<Multiselect v-model="newUser.groups"
+					:options="canAddGroups"
+					:disabled="loading.groups||loading.all"
+					tag-placeholder="create"
+					:placeholder="t('settings', 'Add user in group')"
+					label="name"
+					track-by="id"
+					class="multiselect-vue"
+					:multiple="true"
+					:taggable="true"
+					:close-on-select="false"
+					:tag-width="60"
+					@tag="createGroup">
+					<!-- If user is not admin, he is a subadmin.
+						Subadmins can't create users outside their groups
+						Therefore, empty select is forbidden -->
+					<span slot="noResult">{{ t('settings', 'No results') }}</span>
+				</Multiselect>
+			</div>
+			<div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins">
+				<Multiselect v-model="newUser.subAdminsGroups"
+					:options="subAdminsGroups"
+					:placeholder="t('settings', 'Set user as admin for')"
+					label="name"
+					track-by="id"
+					class="multiselect-vue"
+					:multiple="true"
+					:close-on-select="false"
+					:tag-width="60">
+					<span slot="noResult">{{ t('settings', 'No results') }}</span>
+				</Multiselect>
+			</div>
+			<div class="quota">
+				<Multiselect v-model="newUser.quota"
+					:options="quotaOptions"
+					:placeholder="t('settings', 'Select user quota')"
+					label="label"
+					track-by="id"
+					class="multiselect-vue"
+					:allow-empty="false"
+					:taggable="true"
+					@tag="validateQuota" />
+			</div>
+			<div v-if="showConfig.showLanguages" class="languages">
+				<Multiselect v-model="newUser.language"
+					:options="languages"
+					:placeholder="t('settings', 'Default language')"
+					label="name"
+					track-by="code"
+					class="multiselect-vue"
+					:allow-empty="false"
+					group-values="languages"
+					group-label="label" />
+			</div>
+			<div v-if="showConfig.showStoragePath" class="storageLocation" />
+			<div v-if="showConfig.showUserBackend" class="userBackend" />
+			<div v-if="showConfig.showLastLogin" class="lastLogin" />
+			<div class="userActions">
+				<input id="newsubmit"
+					type="submit"
+					class="button primary icon-checkmark-white has-tooltip"
+					value=""
+					:title="t('settings', 'Add a new user')">
+			</div>
+		</form>
+
+		<user-row v-for="(user, key) in filteredUsers"
+			:key="key"
+			:user="user"
+			:settings="settings"
+			:show-config="showConfig"
+			:groups="groups"
+			:sub-admins-groups="subAdminsGroups"
+			:quota-options="quotaOptions"
+			:languages="languages"
+			:external-actions="externalActions" />
+		<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
+			<div slot="spinner">
+				<div class="users-icon-loading icon-loading" />
+			</div>
+			<div slot="no-more">
+				<div class="users-list-end" />
+			</div>
+			<div slot="no-results">
+				<div id="emptycontent">
+					<div class="icon-contacts-dark" />
+					<h2>{{ t('settings', 'No users in here') }}</h2>
+				</div>
+			</div>
+		</InfiniteLoading>
+	</div>
+</template>
+
+<script>
+import userRow from './userList/UserRow'
+import { Multiselect } from 'nextcloud-vue'
+import InfiniteLoading from 'vue-infinite-loading'
+import Vue from 'vue'
+
+const unlimitedQuota = {
+	id: 'none',
+	label: t('settings', 'Unlimited')
+}
+const defaultQuota = {
+	id: 'default',
+	label: t('settings', 'Default quota')
+}
+const newUser = {
+	id: '',
+	displayName: '',
+	password: '',
+	mailAddress: '',
+	groups: [],
+	subAdminsGroups: [],
+	quota: defaultQuota,
+	language: {
+		code: 'en',
+		name: t('settings', 'Default language')
+	}
+}
+
+export default {
+	name: 'UserList',
+	components: {
+		userRow,
+		Multiselect,
+		InfiniteLoading
+	},
+	props: {
+		users: {
+			type: Array,
+			default: () => []
+		},
+		showConfig: {
+			type: Object,
+			required: true
+		},
+		selectedGroup: {
+			type: String,
+			default: null
+		},
+		externalActions: {
+			type: Array,
+			default: () => []
+		}
+	},
+	data() {
+		return {
+			unlimitedQuota,
+			defaultQuota,
+			loading: {
+				all: false,
+				groups: false
+			},
+			scrolled: false,
+			searchQuery: '',
+			newUser: Object.assign({}, newUser)
+		}
+	},
+	computed: {
+		settings() {
+			return this.$store.getters.getServerData
+		},
+		filteredUsers() {
+			if (this.selectedGroup === 'disabled') {
+				return this.users.filter(user => user.enabled === false)
+			}
+			if (!this.settings.isAdmin) {
+				// we don't want subadmins to edit themselves
+				return this.users.filter(user => user.enabled !== false && user.id !== OC.getCurrentUser().uid)
+			}
+			return this.users.filter(user => user.enabled !== false)
+		},
+		groups() {
+			// data provided php side + remove the disabled group
+			return this.$store.getters.getGroups
+				.filter(group => group.id !== 'disabled')
+				.sort((a, b) => a.name.localeCompare(b.name))
+		},
+		canAddGroups() {
+			// disabled if no permission to add new users to group
+			return this.groups.map(group => {
+				// clone object because we don't want
+				// to edit the original groups
+				group = Object.assign({}, group)
+				group.$isDisabled = group.canAdd === false
+				return group
+			})
+		},
+		subAdminsGroups() {
+			// data provided php side
+			return this.$store.getters.getSubadminGroups
+		},
+		quotaOptions() {
+			// convert the preset array into objects
+			let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({ id: cur, label: cur }), [])
+			// add default presets
+			quotaPreset.unshift(this.unlimitedQuota)
+			quotaPreset.unshift(this.defaultQuota)
+			return quotaPreset
+		},
+		minPasswordLength() {
+			return this.$store.getters.getPasswordPolicyMinLength
+		},
+		usersOffset() {
+			return this.$store.getters.getUsersOffset
+		},
+		usersLimit() {
+			return this.$store.getters.getUsersLimit
+		},
+		usersCount() {
+			return this.users.length
+		},
+
+		/* LANGUAGES */
+		languages() {
+			return [
+				{
+					label: t('settings', 'Common languages'),
+					languages: this.settings.languages.commonlanguages
+				},
+				{
+					label: t('settings', 'All languages'),
+					languages: this.settings.languages.languages
+				}
+			]
+		}
+	},
+	watch: {
+		// watch url change and group select
+		selectedGroup: function(val, old) {
+			// if selected is the disabled group but it's empty
+			this.redirectIfDisabled()
+			this.$store.commit('resetUsers')
+			this.$refs.infiniteLoading.stateChanger.reset()
+			this.setNewUserDefaultGroup(val)
+		},
+
+		// make sure the infiniteLoading state is changed if we manually
+		// add/remove data from the store
+		usersCount: function(val, old) {
+			// deleting the last user, reset the list
+			if (val === 0 && old === 1) {
+				this.$refs.infiniteLoading.stateChanger.reset()
+			// adding the first user, warn the infiniteLoader that
+			// the list is not empty anymore (we don't fetch the newly
+			// added user as we already have all the info we need)
+			} else if (val === 1 && old === 0) {
+				this.$refs.infiniteLoading.stateChanger.loaded()
+			}
+		}
+	},
+	mounted() {
+		if (!this.settings.canChangePassword) {
+			OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
+		}
+
+		/**
+		 * Reset and init new user form
+		 */
+		this.resetForm()
+
+		/**
+		 * Register search
+		 */
+		this.userSearch = new OCA.Search(this.search, this.resetSearch)
+
+		/**
+		 * If disabled group but empty, redirect
+		 */
+		this.redirectIfDisabled()
+	},
+	methods: {
+		onScroll(event) {
+			this.scrolled = event.target.scrollTo > 0
+		},
+
+		/**
+		 * Validate quota string to make sure it's a valid human file size
+		 *
+		 * @param {string} quota Quota in readable format '5 GB'
+		 * @returns {Object}
+		 */
+		validateQuota(quota) {
+			// only used for new presets sent through @Tag
+			let validQuota = OC.Util.computerFileSize(quota)
+			if (validQuota !== null && validQuota >= 0) {
+				// unify format output
+				quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
+				this.newUser.quota = { id: quota, label: quota }
+				return this.newUser.quota
+			}
+			// Default is unlimited
+			this.newUser.quota = this.quotaOptions[0]
+			return this.quotaOptions[0]
+		},
+
+		infiniteHandler($state) {
+			this.$store.dispatch('getUsers', {
+				offset: this.usersOffset,
+				limit: this.usersLimit,
+				group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
+				search: this.searchQuery
+			})
+				.then((response) => { response ? $state.loaded() : $state.complete() })
+		},
+
+		/* SEARCH */
+		search(query) {
+			this.searchQuery = query
+			this.$store.commit('resetUsers')
+			this.$refs.infiniteLoading.stateChanger.reset()
+		},
+		resetSearch() {
+			this.search('')
+		},
+
+		resetForm() {
+			// revert form to original state
+			this.newUser = Object.assign({}, newUser)
+
+			/**
+			 * Init default language from server data. The use of this.settings
+			 * requires a computed variable, which break the v-model binding of the form,
+			 * this is a much easier solution than getter and setter on a computed var
+			 */
+			if (this.settings.defaultLanguage) {
+				Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage)
+			}
+
+			/**
+			 * In case the user directly loaded the user list within a group
+			 * the watch won't be triggered. We need to initialize it.
+			 */
+			this.setNewUserDefaultGroup(this.selectedGroup)
+
+			this.loading.all = false
+		},
+		createUser() {
+			this.loading.all = true
+			this.$store.dispatch('addUser', {
+				userid: this.newUser.id,
+				password: this.newUser.password,
+				displayName: this.newUser.displayName,
+				email: this.newUser.mailAddress,
+				groups: this.newUser.groups.map(group => group.id),
+				subadmin: this.newUser.subAdminsGroups.map(group => group.id),
+				quota: this.newUser.quota.id,
+				language: this.newUser.language.code
+			})
+				.then(() => {
+					this.resetForm()
+					this.$refs.newusername.focus()
+				})
+				.catch((error) => {
+					this.loading.all = false
+					if (error.response && error.response.data && error.response.data.ocs && error.response.data.ocs.meta) {
+						const statuscode = error.response.data.ocs.meta.statuscode
+						if (statuscode === 102) {
+						// wrong username
+							this.$refs.newusername.focus()
+						} else if (statuscode === 107) {
+						// wrong password
+							this.$refs.newuserpassword.focus()
+						}
+					}
+				})
+		},
+		setNewUserDefaultGroup(value) {
+			if (value && value.length > 0) {
+				// setting new user default group to the current selected one
+				let currentGroup = this.groups.find(group => group.id === value)
+				if (currentGroup) {
+					this.newUser.groups = [currentGroup]
+					return
+				}
+			}
+			// fallback, empty selected group
+			this.newUser.groups = []
+		},
+
+		/**
+		 * Create a new group
+		 *
+		 * @param {string} gid Group id
+		 * @returns {Promise}
+		 */
+		createGroup(gid) {
+			this.loading.groups = true
+			this.$store.dispatch('addGroup', gid)
+				.then((group) => {
+					this.newUser.groups.push(this.groups.find(group => group.id === gid))
+					this.loading.groups = false
+				})
+				.catch(() => {
+					this.loading.groups = false
+				})
+			return this.$store.getters.getGroups[this.groups.length]
+		},
+
+		/**
+		 * If the selected group is the disabled group but the count is 0
+		 * redirect to the all users page.
+		 * * we only check for 0 because we don't have the count on ldap
+		 * * and we therefore set the usercount to -1 in this specific case
+		 */
+		redirectIfDisabled() {
+			const allGroups = this.$store.getters.getGroups
+			if (this.selectedGroup === 'disabled'
+				&& allGroups.findIndex(group => group.id === 'disabled' && group.usercount === 0) > -1) {
+				// disabled group is empty, redirection to all users
+				this.$router.push({ name: 'users' })
+				this.$refs.infiniteLoading.stateChanger.reset()
+			}
+		}
+	}
+}
+</script>

+ 80 - 63
apps/settings/src/components/appList.vue

@@ -25,160 +25,177 @@
 		<div id="apps-list" class="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
 			<template v-if="useListView">
 				<transition-group name="app-list" tag="div" class="apps-list-container">
-					<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" />
+					<AppItem v-for="app in apps"
+						:key="app.id"
+						:app="app"
+						:category="category" />
 				</transition-group>
 			</template>
-			<template v-for="bundle in bundles" v-if="useBundleView && bundleApps(bundle.id).length > 0">
-				<transition-group name="app-list" tag="div" class="apps-list-container">
-
-					<div class="apps-header" :key="bundle.id">
-						<div class="app-image"></div>
-						<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" v-on:click="toggleBundle(bundle.id)"></h2>
-						<div class="app-version"></div>
-						<div class="app-level"></div>
-						<div class="app-groups"></div>
-						<div class="actions">&nbsp;</div>
+			<transition-group v-if="useBundleView"
+				name="app-list"
+				tag="div"
+				class="apps-list-container">
+				<template v-for="bundle in bundles">
+					<div :key="bundle.id" class="apps-header">
+						<div class="app-image" />
+						<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" @click="toggleBundle(bundle.id)"></h2>
+						<div class="app-version" />
+						<div class="app-level" />
+						<div class="app-groups" />
+						<div class="actions">
+							&nbsp;
+						</div>
 					</div>
-					<app-item v-for="app in bundleApps(bundle.id)" :key="bundle.id + app.id" :app="app" :category="category"/>
-				</transition-group>
-			</template>
+					<AppItem v-for="app in bundleApps(bundle.id)"
+						:key="bundle.id + app.id"
+						:app="app"
+						:category="category" />
+				</template>
+			</transition-group>
 			<template v-if="useAppStoreView">
-				<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" :list-view="false" />
+				<AppItem v-for="app in apps"
+					:key="app.id"
+					:app="app"
+					:category="category"
+					:list-view="false" />
 			</template>
-
 		</div>
 
 		<div id="apps-list-search" class="apps-list installed">
 			<div class="apps-list-container">
 				<template v-if="search !== '' && searchApps.length > 0">
 					<div class="section">
-						<div></div>
+						<div />
 						<td colspan="5">
 							<h2>{{ t('settings', 'Results from other categories') }}</h2>
 						</td>
 					</div>
-					<app-item v-for="app in searchApps" :key="app.id" :app="app" :category="category" :list-view="true" />
+					<AppItem v-for="app in searchApps"
+						:key="app.id"
+						:app="app"
+						:category="category"
+						:list-view="true" />
 				</template>
 			</div>
 		</div>
 
-		<div id="apps-list-empty" class="emptycontent emptycontent-search" v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0">
-			<div id="app-list-empty-icon" class="icon-settings-dark"></div>
-			<h2>{{ t('settings', 'No apps found for your version')}}</h2>
+		<div v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0" id="apps-list-empty" class="emptycontent emptycontent-search">
+			<div id="app-list-empty-icon" class="icon-settings-dark" />
+			<h2>{{ t('settings', 'No apps found for your version') }}</h2>
 		</div>
 
-		<div id="searchresults"></div>
+		<div id="searchresults" />
 	</div>
 </template>
 
 <script>
-import appItem from './appList/appItem';
-import prefix from './prefixMixin';
+import AppItem from './AppList/AppItem'
+import PrefixMixin from './PrefixMixin'
 
 export default {
-	name: 'appList',
-	mixins: [prefix],
-	props: ['category', 'app', 'search'],
+	name: 'AppList',
 	components: {
-		appItem
+		AppItem
 	},
+	mixins: [PrefixMixin],
+	props: ['category', 'app', 'search'],
 	computed: {
 		loading() {
-			return this.$store.getters.loading('list');
+			return this.$store.getters.loading('list')
 		},
 		apps() {
 			let apps = this.$store.getters.getAllApps
 				.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
-				.sort(function (a, b) {
-					const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name;
-					const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name;
-					return OC.Util.naturalSortCompare(sortStringA, sortStringB);
-				});
+				.sort(function(a, b) {
+					const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name
+					const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name
+					return OC.Util.naturalSortCompare(sortStringA, sortStringB)
+				})
 
 			if (this.category === 'installed') {
-				return apps.filter(app => app.installed);
+				return apps.filter(app => app.installed)
 			}
 			if (this.category === 'enabled') {
-				return apps.filter(app => app.active && app.installed);
+				return apps.filter(app => app.active && app.installed)
 			}
 			if (this.category === 'disabled') {
-				return apps.filter(app => !app.active && app.installed);
+				return apps.filter(app => !app.active && app.installed)
 			}
 			if (this.category === 'app-bundles') {
-				return apps.filter(app => app.bundles);
+				return apps.filter(app => app.bundles)
 			}
 			if (this.category === 'updates') {
-				return apps.filter(app => app.update);
+				return apps.filter(app => app.update)
 			}
 			// filter app store categories
 			return apps.filter(app => {
-				return app.appstore && app.category !== undefined &&
-					(app.category === this.category || app.category.indexOf(this.category) > -1);
-			});
+				return app.appstore && app.category !== undefined
+					&& (app.category === this.category || app.category.indexOf(this.category) > -1)
+			})
 		},
 		bundles() {
-			return this.$store.getters.getServerData.bundles;
+			return this.$store.getters.getServerData.bundles.filter(bundle => this.bundleApps(bundle.id).length > 0)
 		},
 		bundleApps() {
 			return function(bundle) {
 				return this.$store.getters.getAllApps
-					.filter(app => app.bundleId === bundle);
+					.filter(app => app.bundleId === bundle)
 			}
 		},
 		searchApps() {
 			if (this.search === '') {
-				return [];
+				return []
 			}
 			return this.$store.getters.getAllApps
 				.filter(app => {
 					if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
-						return (!this.apps.find(_app => _app.id === app.id));
+						return (!this.apps.find(_app => _app.id === app.id))
 					}
-					return false;
-				});
+					return false
+				})
 		},
 		useAppStoreView() {
-			return !this.useListView && !this.useBundleView;
+			return !this.useListView && !this.useBundleView
 		},
 		useListView() {
-			return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates');
+			return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates')
 		},
 		useBundleView() {
-			return (this.category === 'app-bundles');
+			return (this.category === 'app-bundles')
 		},
 		allBundlesEnabled() {
-			let self = this;
+			let self = this
 			return function(id) {
-				return self.bundleApps(id).filter(app => !app.active).length === 0;
+				return self.bundleApps(id).filter(app => !app.active).length === 0
 			}
 		},
 		bundleToggleText() {
-			let self = this;
+			let self = this
 			return function(id) {
 				if (self.allBundlesEnabled(id)) {
-					return t('settings', 'Disable all');
+					return t('settings', 'Disable all')
 				}
-				return t('settings', 'Enable all');
+				return t('settings', 'Enable all')
 			}
 		}
 	},
 	methods: {
 		toggleBundle(id) {
 			if (this.allBundlesEnabled(id)) {
-				return this.disableBundle(id);
+				return this.disableBundle(id)
 			}
-			return this.enableBundle(id);
+			return this.enableBundle(id)
 		},
 		enableBundle(id) {
-			let apps = this.bundleApps(id).map(app => app.id);
+			let apps = this.bundleApps(id).map(app => app.id)
 			this.$store.dispatch('enableApp', { appId: apps, groups: [] })
-				.catch((error) => { console.log(error); OC.Notification.show(error)});
+				.catch((error) => { console.error(error); OC.Notification.show(error) })
 		},
 		disableBundle(id) {
-			let apps = this.bundleApps(id).map(app => app.id);
+			let apps = this.bundleApps(id).map(app => app.id)
 			this.$store.dispatch('disableApp', { appId: apps, groups: [] })
-				.catch((error) => { OC.Notification.show(error)});
+				.catch((error) => { OC.Notification.show(error) })
 		}
-	},
+	}
 }
 </script>

+ 12 - 12
apps/settings/src/components/appList/appScore.vue

@@ -21,18 +21,18 @@
   -->
 
 <template>
-	<img :src="scoreImage" class="app-score-image" />
+	<img :src="scoreImage" class="app-score-image">
 </template>
 <script>
-	export default {
-		name: 'appScore',
-		props: ['score'],
-		computed: {
-			scoreImage() {
-				let score = Math.round( this.score * 10 );
-				let imageName = 'rating/s' + score + '.svg';
-				return OC.imagePath('core', imageName);
-			}
+export default {
+	name: 'AppScore',
+	props: ['score'],
+	computed: {
+		scoreImage() {
+			let score = Math.round(this.score * 10)
+			let imageName = 'rating/s' + score + '.svg'
+			return OC.imagePath('core', imageName)
 		}
-	};
-</script>
+	}
+}
+</script>

+ 7 - 7
apps/settings/src/components/prefixMixin.vue

@@ -21,12 +21,12 @@
   -->
 
 <script>
-	export default {
-		name: 'prefixMixin',
-		methods: {
-			prefix (prefix, content) {
-				return prefix + '_' + content;
-			},
+export default {
+	name: 'PrefixMixin',
+	methods: {
+		prefix(prefix, content) {
+			return prefix + '_' + content
 		}
 	}
-</script>
+}
+</script>

+ 16 - 16
apps/settings/src/components/svgFilterMixin.vue

@@ -21,20 +21,20 @@
   -->
 
 <script>
-	export default {
-		name: 'svgFilterMixin',
-		mounted() {
-			this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100 )) + new Date().getSeconds() + new Date().getMilliseconds();
-		},
-		computed: {
-			filterUrl () {
-				return `url(#${this.filterId})`;
-			},
-		},
-		data() {
-			return {
-				filterId: '',
-			};
-		},
+export default {
+	name: 'SvgFilterMixin',
+	data() {
+		return {
+			filterId: ''
+		}
+	},
+	computed: {
+		filterUrl() {
+			return `url(#${this.filterId})`
+		}
+	},
+	mounted() {
+		this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100)) + new Date().getSeconds() + new Date().getMilliseconds()
 	}
-</script>
+}
+</script>

+ 706 - 0
apps/settings/src/components/userList/UserRow.vue

@@ -0,0 +1,706 @@
+<!--
+  - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @author John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @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>
+	<!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
+	<div v-if="Object.keys(user).length ===1" class="row" :data-id="user.id">
+		<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
+			<img v-if="!loading.delete && !loading.disable && !loading.wipe"
+				alt=""
+				width="32"
+				height="32"
+				:src="generateAvatar(user.id, 32)"
+				:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
+		</div>
+		<div class="name">
+			{{ user.id }}
+		</div>
+		<div class="obfuscated">
+			{{ t('settings','You do not have permissions to see the details of this user') }}
+		</div>
+	</div>
+
+	<!-- User full data -->
+	<div v-else
+		class="row"
+		:class="{'disabled': loading.delete || loading.disable}"
+		:data-id="user.id">
+		<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
+			<img v-if="!loading.delete && !loading.disable && !loading.wipe"
+				alt=""
+				width="32"
+				height="32"
+				:src="generateAvatar(user.id, 32)"
+				:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
+		</div>
+		<!-- dirty hack to ellipsis on two lines -->
+		<div class="name">
+			{{ user.id }}
+		</div>
+		<form class="displayName" :class="{'icon-loading-small': loading.displayName}" @submit.prevent="updateDisplayName">
+			<template v-if="user.backendCapabilities.setDisplayName">
+				<input v-if="user.backendCapabilities.setDisplayName"
+					:id="'displayName'+user.id+rand"
+					ref="displayName"
+					type="text"
+					:disabled="loading.displayName||loading.all"
+					:value="user.displayname"
+					autocomplete="new-password"
+					autocorrect="off"
+					autocapitalize="off"
+					spellcheck="false">
+				<input v-if="user.backendCapabilities.setDisplayName"
+					type="submit"
+					class="icon-confirm"
+					value="">
+			</template>
+			<div v-else v-tooltip.auto="t('settings', 'The backend does not support changing the display name')" class="name">
+				{{ user.displayname }}
+			</div>
+		</form>
+		<form v-if="settings.canChangePassword && user.backendCapabilities.setPassword"
+			class="password"
+			:class="{'icon-loading-small': loading.password}"
+			@submit.prevent="updatePassword">
+			<input :id="'password'+user.id+rand"
+				ref="password"
+				type="password"
+				required
+				:disabled="loading.password||loading.all"
+				:minlength="minPasswordLength"
+				value=""
+				:placeholder="t('settings', 'New password')"
+				autocomplete="new-password"
+				autocorrect="off"
+				autocapitalize="off"
+				spellcheck="false">
+			<input type="submit" class="icon-confirm" value="">
+		</form>
+		<div v-else />
+		<form class="mailAddress" :class="{'icon-loading-small': loading.mailAddress}" @submit.prevent="updateEmail">
+			<input :id="'mailAddress'+user.id+rand"
+				ref="mailAddress"
+				type="email"
+				:disabled="loading.mailAddress||loading.all"
+				:value="user.email"
+				autocomplete="new-password"
+				autocorrect="off"
+				autocapitalize="off"
+				spellcheck="false">
+			<input type="submit" class="icon-confirm" value="">
+		</form>
+		<div class="groups" :class="{'icon-loading-small': loading.groups}">
+			<Multiselect :value="userGroups"
+				:options="availableGroups"
+				:disabled="loading.groups||loading.all"
+				tag-placeholder="create"
+				:placeholder="t('settings', 'Add user in group')"
+				label="name"
+				track-by="id"
+				class="multiselect-vue"
+				:limit="2"
+				:multiple="true"
+				:taggable="settings.isAdmin"
+				:close-on-select="false"
+				:tag-width="60"
+				@tag="createGroup"
+				@select="addUserGroup"
+				@remove="removeUserGroup">
+				<span slot="limit" v-tooltip.auto="formatGroupsTitle(userGroups)" class="multiselect__limit">+{{ userGroups.length-2 }}</span>
+				<span slot="noResult">{{ t('settings', 'No results') }}</span>
+			</Multiselect>
+		</div>
+		<div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins" :class="{'icon-loading-small': loading.subadmins}">
+			<Multiselect :value="userSubAdminsGroups"
+				:options="subAdminsGroups"
+				:disabled="loading.subadmins||loading.all"
+				:placeholder="t('settings', 'Set user as admin for')"
+				label="name"
+				track-by="id"
+				class="multiselect-vue"
+				:limit="2"
+				:multiple="true"
+				:close-on-select="false"
+				:tag-width="60"
+				@select="addUserSubAdmin"
+				@remove="removeUserSubAdmin">
+				<span slot="limit" v-tooltip.auto="formatGroupsTitle(userSubAdminsGroups)" class="multiselect__limit">+{{ userSubAdminsGroups.length-2 }}</span>
+				<span slot="noResult">{{ t('settings', 'No results') }}</span>
+			</Multiselect>
+		</div>
+		<div v-tooltip.auto="usedSpace" class="quota" :class="{'icon-loading-small': loading.quota}">
+			<Multiselect :value="userQuota"
+				:options="quotaOptions"
+				:disabled="loading.quota||loading.all"
+				tag-placeholder="create"
+				:placeholder="t('settings', 'Select user quota')"
+				label="label"
+				track-by="id"
+				class="multiselect-vue"
+				:allow-empty="false"
+				:taggable="true"
+				@tag="validateQuota"
+				@input="setUserQuota" />
+			<progress class="quota-user-progress"
+				:class="{'warn':usedQuota>80}"
+				:value="usedQuota"
+				max="100" />
+		</div>
+		<div v-if="showConfig.showLanguages"
+			class="languages"
+			:class="{'icon-loading-small': loading.languages}">
+			<Multiselect :value="userLanguage"
+				:options="languages"
+				:disabled="loading.languages||loading.all"
+				:placeholder="t('settings', 'No language set')"
+				label="name"
+				track-by="code"
+				class="multiselect-vue"
+				:allow-empty="false"
+				group-values="languages"
+				group-label="label"
+				@input="setUserLanguage" />
+		</div>
+		<div v-if="showConfig.showStoragePath" class="storageLocation">
+			{{ user.storageLocation }}
+		</div>
+		<div v-if="showConfig.showUserBackend" class="userBackend">
+			{{ user.backend }}
+		</div>
+		<div v-if="showConfig.showLastLogin" v-tooltip.auto="user.lastLogin>0 ? OC.Util.formatDate(user.lastLogin) : ''" class="lastLogin">
+			{{ user.lastLogin>0 ? OC.Util.relativeModifiedDate(user.lastLogin) : t('settings','Never') }}
+		</div>
+		<div class="userActions">
+			<div v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all" class="toggleUserActions">
+				<div v-click-outside="hideMenu" class="icon-more" @click="toggleMenu" />
+				<div class="popovermenu" :class="{ 'open': openedMenu }">
+					<PopoverMenu :menu="userActions" />
+				</div>
+			</div>
+			<div class="feedback" :style="{opacity: feedbackMessage !== '' ? 1 : 0}">
+				<div class="icon-checkmark" />
+				{{ feedbackMessage }}
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import ClickOutside from 'vue-click-outside'
+import Vue from 'vue'
+import VTooltip from 'v-tooltip'
+import { PopoverMenu, Multiselect } from 'nextcloud-vue'
+
+Vue.use(VTooltip)
+
+export default {
+	name: 'UserRow',
+	components: {
+		PopoverMenu,
+		Multiselect
+	},
+	directives: {
+		ClickOutside
+	},
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+		settings: {
+			type: Object,
+			default: () => ({})
+		},
+		groups: {
+			type: Array,
+			default: () => []
+		},
+		subAdminsGroups: {
+			type: Array,
+			default: () => []
+		},
+		quotaOptions: {
+			type: Array,
+			default: () => []
+		},
+		showConfig: {
+			type: Object,
+			default: () => ({})
+		},
+		languages: {
+			type: Array,
+			required: true
+		},
+		externalActions: {
+			type: Array,
+			default: () => []
+		}
+	},
+	data() {
+		return {
+			rand: parseInt(Math.random() * 1000),
+			openedMenu: false,
+			feedbackMessage: '',
+			loading: {
+				all: false,
+				displayName: false,
+				password: false,
+				mailAddress: false,
+				groups: false,
+				subadmins: false,
+				quota: false,
+				delete: false,
+				disable: false,
+				languages: false,
+				wipe: false
+			}
+		}
+	},
+	computed: {
+		/* USER POPOVERMENU ACTIONS */
+		userActions() {
+			let actions = [
+				{
+					icon: 'icon-delete',
+					text: t('settings', 'Delete user'),
+					action: this.deleteUser
+				},
+				{
+					icon: 'icon-delete',
+					text: t('settings', 'Wipe all devices'),
+					action: this.wipeUserDevices
+				},
+				{
+					icon: this.user.enabled ? 'icon-close' : 'icon-add',
+					text: this.user.enabled ? t('settings', 'Disable user') : t('settings', 'Enable user'),
+					action: this.enableDisableUser
+				}
+			]
+			if (this.user.email !== null && this.user.email !== '') {
+				actions.push({
+					icon: 'icon-mail',
+					text: t('settings', 'Resend welcome email'),
+					action: this.sendWelcomeMail
+				})
+			}
+			return actions.concat(this.externalActions)
+		},
+
+		/* GROUPS MANAGEMENT */
+		userGroups() {
+			let userGroups = this.groups.filter(group => this.user.groups.includes(group.id))
+			return userGroups
+		},
+		userSubAdminsGroups() {
+			let userSubAdminsGroups = this.subAdminsGroups.filter(group => this.user.subadmin.includes(group.id))
+			return userSubAdminsGroups
+		},
+		availableGroups() {
+			return this.groups.map((group) => {
+				// clone object because we don't want
+				// to edit the original groups
+				let groupClone = Object.assign({}, group)
+
+				// two settings here:
+				// 1. user NOT in group but no permission to add
+				// 2. user is in group but no permission to remove
+				groupClone.$isDisabled
+					= (group.canAdd === false
+						&& !this.user.groups.includes(group.id))
+					|| (group.canRemove === false
+						&& this.user.groups.includes(group.id))
+				return groupClone
+			})
+		},
+
+		/* QUOTA MANAGEMENT */
+		usedSpace() {
+			if (this.user.quota.used) {
+				return t('settings', '{size} used', { size: OC.Util.humanFileSize(this.user.quota.used) })
+			}
+			return t('settings', '{size} used', { size: OC.Util.humanFileSize(0) })
+		},
+		usedQuota() {
+			let quota = this.user.quota.quota
+			if (quota > 0) {
+				quota = Math.min(100, Math.round(this.user.quota.used / quota * 100))
+			} else {
+				var usedInGB = this.user.quota.used / (10 * Math.pow(2, 30))
+				// asymptotic curve approaching 50% at 10GB to visualize used stace with infinite quota
+				quota = 95 * (1 - (1 / (usedInGB + 1)))
+			}
+			return isNaN(quota) ? 0 : quota
+		},
+		// Mapping saved values to objects
+		userQuota() {
+			if (this.user.quota.quota >= 0) {
+				// if value is valid, let's map the quotaOptions or return custom quota
+				let humanQuota = OC.Util.humanFileSize(this.user.quota.quota)
+				let userQuota = this.quotaOptions.find(quota => quota.id === humanQuota)
+				return userQuota || { id: humanQuota, label: humanQuota }
+			} else if (this.user.quota.quota === 'default') {
+				// default quota is replaced by the proper value on load
+				return this.quotaOptions[0]
+			}
+			return this.quotaOptions[1] // unlimited
+		},
+
+		/* PASSWORD POLICY? */
+		minPasswordLength() {
+			return this.$store.getters.getPasswordPolicyMinLength
+		},
+
+		/* LANGUAGE */
+		userLanguage() {
+			let availableLanguages = this.languages[0].languages.concat(this.languages[1].languages)
+			let userLang = availableLanguages.find(lang => lang.code === this.user.language)
+			if (typeof userLang !== 'object' && this.user.language !== '') {
+				return {
+					code: this.user.language,
+					name: this.user.language
+				}
+			} else if (this.user.language === '') {
+				return false
+			}
+			return userLang
+		}
+	},
+	mounted() {
+		// required if popup needs to stay opened after menu click
+		// since we only have disable/delete actions, let's close it directly
+		// this.popupItem = this.$el;
+	},
+	methods: {
+		/* MENU HANDLING */
+		toggleMenu() {
+			this.openedMenu = !this.openedMenu
+		},
+		hideMenu() {
+			this.openedMenu = false
+		},
+
+		/**
+		 * Generate avatar url
+		 *
+		 * @param {string} user The user name
+		 * @param {int} size Size integer, default 32
+		 * @returns {string}
+		 */
+		generateAvatar(user, size = 32) {
+			return OC.generateUrl(
+				'/avatar/{user}/{size}?v={version}',
+				{
+					user: user,
+					size: size,
+					version: oc_userconfig.avatar.version
+				}
+			)
+		},
+
+		/**
+		 * Format array of groups objects to a string for the popup
+		 *
+		 * @param {array} groups The groups
+		 * @returns {string}
+		 */
+		formatGroupsTitle(groups) {
+			let names = groups.map(group => group.name)
+			return names.slice(2).join(', ')
+		},
+
+		wipeUserDevices() {
+			this.loading.wipe = true
+			this.loading.all = true
+			let userid = this.user.id
+			return this.$store.dispatch('wipeUserDevices', userid)
+				.then(() => {
+					this.loading.wipe = false
+					this.loading.all = false
+				})
+		},
+
+		deleteUser() {
+			this.loading.delete = true
+			this.loading.all = true
+			let userid = this.user.id
+			return this.$store.dispatch('deleteUser', userid)
+				.then(() => {
+					this.loading.delete = false
+					this.loading.all = false
+				})
+		},
+
+		enableDisableUser() {
+			this.loading.delete = true
+			this.loading.all = true
+			let userid = this.user.id
+			let enabled = !this.user.enabled
+			return this.$store.dispatch('enableDisableUser', { userid, enabled })
+				.then(() => {
+					this.loading.delete = false
+					this.loading.all = false
+				})
+		},
+
+		/**
+		 * Set user displayName
+		 *
+		 * @param {string} displayName The display name
+		 */
+		updateDisplayName() {
+			let displayName = this.$refs.displayName.value
+			this.loading.displayName = true
+			this.$store.dispatch('setUserData', {
+				userid: this.user.id,
+				key: 'displayname',
+				value: displayName
+			}).then(() => {
+				this.loading.displayName = false
+				this.$refs.displayName.value = displayName
+			})
+		},
+
+		/**
+		 * Set user password
+		 *
+		 * @param {string} password The email adress
+		 */
+		updatePassword() {
+			let password = this.$refs.password.value
+			this.loading.password = true
+			this.$store.dispatch('setUserData', {
+				userid: this.user.id,
+				key: 'password',
+				value: password
+			}).then(() => {
+				this.loading.password = false
+				this.$refs.password.value = '' // empty & show placeholder
+			})
+		},
+
+		/**
+		 * Set user mailAddress
+		 *
+		 * @param {string} mailAddress The email adress
+		 */
+		updateEmail() {
+			let mailAddress = this.$refs.mailAddress.value
+			this.loading.mailAddress = true
+			this.$store.dispatch('setUserData', {
+				userid: this.user.id,
+				key: 'email',
+				value: mailAddress
+			}).then(() => {
+				this.loading.mailAddress = false
+				this.$refs.mailAddress.value = mailAddress
+			})
+		},
+
+		/**
+		 * Create a new group and add user to it
+		 *
+		 * @param {string} gid Group id
+		 */
+		async createGroup(gid) {
+			this.loading = { groups: true, subadmins: true }
+			try {
+				await this.$store.dispatch('addGroup', gid)
+				let userid = this.user.id
+				await this.$store.dispatch('addUserGroup', { userid, gid })
+			} catch (error) {
+				console.error(error)
+			} finally {
+				this.loading = { groups: false, subadmins: false }
+			}
+			return this.$store.getters.getGroups[this.groups.length]
+		},
+
+		/**
+		 * Add user to group
+		 *
+		 * @param {object} group Group object
+		 */
+		async addUserGroup(group) {
+			if (group.canAdd === false) {
+				return false
+			}
+			this.loading.groups = true
+			let userid = this.user.id
+			let gid = group.id
+			try {
+				await this.$store.dispatch('addUserGroup', { userid, gid })
+			} catch (error) {
+				console.error(error)
+			} finally {
+				this.loading.groups = false
+			}
+		},
+
+		/**
+		 * Remove user from group
+		 *
+		 * @param {object} group Group object
+		 */
+		async removeUserGroup(group) {
+			if (group.canRemove === false) {
+				return false
+			}
+
+			this.loading.groups = true
+			let userid = this.user.id
+			let gid = group.id
+
+			try {
+				await this.$store.dispatch('removeUserGroup', { userid, gid })
+				this.loading.groups = false
+				// remove user from current list if current list is the removed group
+				if (this.$route.params.selectedGroup === gid) {
+					this.$store.commit('deleteUser', userid)
+				}
+			} catch {
+				this.loading.groups = false
+			}
+		},
+
+		/**
+		 * Add user to group
+		 *
+		 * @param {object} group Group object
+		 */
+		async addUserSubAdmin(group) {
+			this.loading.subadmins = true
+			let userid = this.user.id
+			let gid = group.id
+
+			try {
+				await this.$store.dispatch('addUserSubAdmin', { userid, gid })
+				this.loading.subadmins = false
+			} catch (error) {
+				console.error(error)
+			}
+		},
+
+		/**
+		 * Remove user from group
+		 *
+		 * @param {object} group Group object
+		 */
+		async removeUserSubAdmin(group) {
+			this.loading.subadmins = true
+			let userid = this.user.id
+			let gid = group.id
+
+			try {
+				await this.$store.dispatch('removeUserSubAdmin', { userid, gid })
+			} catch (error) {
+				console.error(error)
+			} finally {
+				this.loading.subadmins = false
+			}
+		},
+
+		/**
+		 * Dispatch quota set request
+		 *
+		 * @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
+		 * @returns {string}
+		 */
+		async setUserQuota(quota = 'none') {
+			this.loading.quota = true
+			// ensure we only send the preset id
+			quota = quota.id ? quota.id : quota
+
+			try {
+				await this.$store.dispatch('setUserData', {
+					userid: this.user.id,
+					key: 'quota',
+					value: quota
+				})
+			} catch (error) {
+				console.error(error)
+			} finally {
+				this.loading.quota = false
+			}
+			return quota
+		},
+
+		/**
+		 * Validate quota string to make sure it's a valid human file size
+		 *
+		 * @param {string} quota Quota in readable format '5 GB'
+		 * @returns {Promise|boolean}
+		 */
+		validateQuota(quota) {
+			// only used for new presets sent through @Tag
+			let validQuota = OC.Util.computerFileSize(quota)
+			if (validQuota !== null && validQuota >= 0) {
+				// unify format output
+				return this.setUserQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)))
+			}
+			// if no valid do not change
+			return false
+		},
+
+		/**
+		 * Dispatch language set request
+		 *
+		 * @param {Object} lang language object {code:'en', name:'English'}
+		 * @returns {Object}
+		 */
+		async setUserLanguage(lang) {
+			this.loading.languages = true
+			// ensure we only send the preset id
+			try {
+				await this.$store.dispatch('setUserData', {
+					userid: this.user.id,
+					key: 'language',
+					value: lang.code
+				})
+			} catch (error) {
+				console.error(error)
+			} finally {
+				this.loading.languages = false
+			}
+			return lang
+		},
+
+		/**
+		 * Dispatch new welcome mail request
+		 */
+		sendWelcomeMail() {
+			this.loading.all = true
+			this.$store.dispatch('sendWelcomeMail', this.user.id)
+				.then(success => {
+					if (success) {
+						// Show feedback to indicate the success
+						this.feedbackMessage = t('setting', 'Welcome mail sent!')
+						setTimeout(() => {
+							this.feedbackMessage = ''
+						}, 2000)
+					}
+					this.loading.all = false
+				})
+		}
+
+	}
+}
+</script>

+ 0 - 574
apps/settings/src/components/userList/userRow.vue

@@ -1,574 +0,0 @@
-<!--
-  - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
-  -
-  - @author John Molakvoæ <skjnldsv@protonmail.com>
-  -
-  - @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>
-	<!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
-	<div class="row" v-if="Object.keys(user).length ===1" :data-id="user.id">
-		<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
-			<img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
-				 :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
-				 v-if="!loading.delete && !loading.disable && !loading.wipe">
-		</div>
-		<div class="name">{{user.id}}</div>
-		<div class="obfuscated">{{t('settings','You do not have permissions to see the details of this user')}}</div>
-	</div>
-
-	<!-- User full data -->
-	<div class="row" v-else :class="{'disabled': loading.delete || loading.disable}" :data-id="user.id">
-		<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
-			<img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
-				 :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
-				 v-if="!loading.delete && !loading.disable && !loading.wipe">
-		</div>
-		<!-- dirty hack to ellipsis on two lines -->
-		<div class="name">{{user.id}}</div>
-		<form class="displayName" :class="{'icon-loading-small': loading.displayName}" v-on:submit.prevent="updateDisplayName">
-			<template v-if="user.backendCapabilities.setDisplayName">
-				<input v-if="user.backendCapabilities.setDisplayName"
-						:id="'displayName'+user.id+rand" type="text"
-						:disabled="loading.displayName||loading.all"
-						:value="user.displayname" ref="displayName"
-						autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
-				<input v-if="user.backendCapabilities.setDisplayName" type="submit" class="icon-confirm" value="" />
-			</template>
-			<div v-else class="name" v-tooltip.auto="t('settings', 'The backend does not support changing the display name')">{{user.displayname}}</div>
-		</form>
-		<form class="password" v-if="settings.canChangePassword && user.backendCapabilities.setPassword" :class="{'icon-loading-small': loading.password}"
-			  v-on:submit.prevent="updatePassword">
-			<input :id="'password'+user.id+rand" type="password" required
-					:disabled="loading.password||loading.all" :minlength="minPasswordLength"
-					value="" :placeholder="t('settings', 'New password')" ref="password"
-					autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
-			<input type="submit" class="icon-confirm" value="" />
-		</form>
-		<div v-else></div>
-		<form class="mailAddress" :class="{'icon-loading-small': loading.mailAddress}" v-on:submit.prevent="updateEmail">
-			<input :id="'mailAddress'+user.id+rand" type="email"
-					:disabled="loading.mailAddress||loading.all"
-					:value="user.email" ref="mailAddress"
-					autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
-			<input type="submit" class="icon-confirm" value="" />
-		</form>
-		<div class="groups" :class="{'icon-loading-small': loading.groups}">
-			<multiselect :value="userGroups" :options="availableGroups" :disabled="loading.groups||loading.all"
-						 tag-placeholder="create" :placeholder="t('settings', 'Add user in group')"
-						 label="name" track-by="id" class="multiselect-vue" :limit="2"
-						 :multiple="true" :taggable="settings.isAdmin" :closeOnSelect="false"
-						 :tag-width="60"
-						 @tag="createGroup" @select="addUserGroup" @remove="removeUserGroup">
-				<span slot="limit" class="multiselect__limit" v-tooltip.auto="formatGroupsTitle(userGroups)">+{{userGroups.length-2}}</span>
-				<span slot="noResult">{{t('settings', 'No results')}}</span>
-			</multiselect>
-		</div>
-		<div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin" :class="{'icon-loading-small': loading.subadmins}">
-			<multiselect :value="userSubAdminsGroups" :options="subAdminsGroups" :disabled="loading.subadmins||loading.all"
-						 :placeholder="t('settings', 'Set user as admin for')"
-						 label="name" track-by="id" class="multiselect-vue" :limit="2"
-						 :multiple="true" :closeOnSelect="false" :tag-width="60"
-						 @select="addUserSubAdmin" @remove="removeUserSubAdmin">
-				<span slot="limit" class="multiselect__limit" v-tooltip.auto="formatGroupsTitle(userSubAdminsGroups)">+{{userSubAdminsGroups.length-2}}</span>
-				<span slot="noResult">{{t('settings', 'No results')}}</span>
-			</multiselect>
-		</div>
-		<div class="quota" :class="{'icon-loading-small': loading.quota}" v-tooltip.auto="usedSpace">
-			<multiselect :value="userQuota" :options="quotaOptions" :disabled="loading.quota||loading.all"
-						 tag-placeholder="create" :placeholder="t('settings', 'Select user quota')"
-						 label="label" track-by="id" class="multiselect-vue"
-						 :allowEmpty="false" :taggable="true"
-						 @tag="validateQuota" @input="setUserQuota">
-			</multiselect>
-			<progress class="quota-user-progress" :class="{'warn':usedQuota>80}" :value="usedQuota" max="100"></progress>
-		</div>
-		<div class="languages" :class="{'icon-loading-small': loading.languages}"
-			 v-if="showConfig.showLanguages">
-			<multiselect :value="userLanguage" :options="languages" :disabled="loading.languages||loading.all"
-						 :placeholder="t('settings', 'No language set')"
-						 label="name" track-by="code" class="multiselect-vue"
-						 :allowEmpty="false" group-values="languages" group-label="label"
-						 @input="setUserLanguage">
-			</multiselect>
-		</div>
-		<div class="storageLocation" v-if="showConfig.showStoragePath">{{user.storageLocation}}</div>
-		<div class="userBackend" v-if="showConfig.showUserBackend">{{user.backend}}</div>
-		<div class="lastLogin" v-if="showConfig.showLastLogin" v-tooltip.auto="user.lastLogin>0 ? OC.Util.formatDate(user.lastLogin) : ''">
-			{{user.lastLogin>0 ? OC.Util.relativeModifiedDate(user.lastLogin) : t('settings','Never')}}
-		</div>
-		<div class="userActions">
-			<div class="toggleUserActions" v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all">
-				<div class="icon-more" v-click-outside="hideMenu" @click="toggleMenu"></div>
-				<div class="popovermenu" :class="{ 'open': openedMenu }">
-					<popover-menu :menu="userActions" />
-				</div>
-			</div>
-			<div class="feedback" :style="{opacity: feedbackMessage !== '' ? 1 : 0}">
-				<div class="icon-checkmark"></div>
-				{{feedbackMessage}}
-			</div>
-		</div>
-		</div>
-</template>
-
-<script>
-import ClickOutside from 'vue-click-outside';
-import Vue from 'vue'
-import VTooltip from 'v-tooltip'
-import { PopoverMenu, Multiselect } from 'nextcloud-vue'
-
-Vue.use(VTooltip)
-
-export default {
-	name: 'userRow',
-	props: ['user', 'settings', 'groups', 'subAdminsGroups', 'quotaOptions', 'showConfig', 'languages', 'externalActions'],
-	components: {
-		PopoverMenu,
-		Multiselect
-	},
-	directives: {
-		ClickOutside
-	},
-	mounted() {
-		// required if popup needs to stay opened after menu click
-		// since we only have disable/delete actions, let's close it directly
-		// this.popupItem = this.$el;
-	},
-	data() {
-		return {
-			rand: parseInt(Math.random() * 1000),
-			openedMenu: false,
-			feedbackMessage: '',
-			loading: {
-				all: false,
-				displayName: false,
-				password: false,
-				mailAddress: false,
-				groups: false,
-				subadmins: false,
-				quota: false,
-				delete: false,
-				disable: false,
-				languages: false,
-				wipe: false,
-			}
-		}
-	},
-	computed: {
-		/* USER POPOVERMENU ACTIONS */
-		userActions() {
-			let actions = [
-				{
-					icon: 'icon-delete',
-					text: t('settings', 'Delete user'),
-					action: this.deleteUser,
-				},
-				{
-					icon: 'icon-delete',
-					text: t('settings', 'Wipe all devices'),
-					action: this.wipeUserDevices,
-				},
-				{
-					icon: this.user.enabled ? 'icon-close' : 'icon-add',
-					text: this.user.enabled ? t('settings', 'Disable user') : t('settings', 'Enable user'),
-					action: this.enableDisableUser,
-				},
-			];
-			if (this.user.email !== null && this.user.email !== '') {
-				actions.push({
-					icon: 'icon-mail',
-					text: t('settings','Resend welcome email'),
-					action: this.sendWelcomeMail
-				})
-			}
-			return actions.concat(this.externalActions);
-		},
-
-		/* GROUPS MANAGEMENT */
-		userGroups() {
-			let userGroups = this.groups.filter(group => this.user.groups.includes(group.id));
-			return userGroups;
-		},
-		userSubAdminsGroups() {
-			let userSubAdminsGroups = this.subAdminsGroups.filter(group => this.user.subadmin.includes(group.id));
-			return userSubAdminsGroups;
-		},
-		availableGroups() {
-			return this.groups.map((group) => {
-				// clone object because we don't want
-				// to edit the original groups
-				let groupClone = Object.assign({}, group);
-
-				// two settings here:
-				// 1. user NOT in group but no permission to add
-				// 2. user is in group but no permission to remove
-				groupClone.$isDisabled =
-					(group.canAdd === false &&
-						!this.user.groups.includes(group.id)) ||
-					(group.canRemove === false &&
-						this.user.groups.includes(group.id));
-				return groupClone;
-			});
-		},
-
-		/* QUOTA MANAGEMENT */
-		usedSpace() {
-			if (this.user.quota.used) {
-				return t('settings', '{size} used', {size: OC.Util.humanFileSize(this.user.quota.used)});
-			}
-			return t('settings', '{size} used', {size: OC.Util.humanFileSize(0)});
-		},
-		usedQuota() {
-			let quota = this.user.quota.quota;
-			if (quota > 0) {
-				quota = Math.min(100, Math.round(this.user.quota.used / quota * 100));
-			} else {
-				var usedInGB = this.user.quota.used / (10 * Math.pow(2, 30));
-				//asymptotic curve approaching 50% at 10GB to visualize used stace with infinite quota
-				quota = 95 * (1 - (1 / (usedInGB + 1)));
-			}
-			return isNaN(quota) ? 0 : quota;
-		},
-		// Mapping saved values to objects
-		userQuota() {
-			if (this.user.quota.quota >= 0) {
-				// if value is valid, let's map the quotaOptions or return custom quota
-				let humanQuota = OC.Util.humanFileSize(this.user.quota.quota);
-				let userQuota = this.quotaOptions.find(quota => quota.id === humanQuota);
-				return userQuota ? userQuota : {id:humanQuota, label:humanQuota};
-			} else if (this.user.quota.quota === 'default') {
-				// default quota is replaced by the proper value on load
-				return this.quotaOptions[0];
-			}
-			return this.quotaOptions[1]; // unlimited
-		},
-
-		/* PASSWORD POLICY? */
-		minPasswordLength() {
-			return this.$store.getters.getPasswordPolicyMinLength;
-		},
-
-		/* LANGUAGE */
-		userLanguage() {
-			let availableLanguages = this.languages[0].languages.concat(this.languages[1].languages);
-			let userLang = availableLanguages.find(lang => lang.code === this.user.language);
-			if (typeof userLang !== 'object' && this.user.language !== '') {
-				return {
-					code: this.user.language,
-					name: this.user.language
-				}
-			} else if(this.user.language === '') {
-				return false;
-			}
-			return userLang;
-		}
-	},
-	methods: {
-		/* MENU HANDLING */
-		toggleMenu() {
-			this.openedMenu = !this.openedMenu;
-		},
-		hideMenu() {
-			this.openedMenu = false;
-		},
-
-		/**
-		 * Generate avatar url
-		 * 
-		 * @param {string} user The user name
-		 * @param {int} size Size integer, default 32
-		 * @returns {string}
-		 */
-		generateAvatar(user, size=32) {
-			return OC.generateUrl(
-				'/avatar/{user}/{size}?v={version}',
-				{
-					user: user,
-					size: size,
-					version: oc_userconfig.avatar.version
-				}
-			);
-		},
-
-		/**
-		 * Format array of groups objects to a string for the popup
-		 * 
-		 * @param {array} groups The groups
-		 * @returns {string}
-		 */
-		formatGroupsTitle(groups) {
-			let names = groups.map(group => group.name);
-			return names.slice(2,).join(', ');
-		},
-
-		wipeUserDevices() {
-			this.loading.wipe = true;
-			this.loading.all = true;
-			let userid = this.user.id;
-			return this.$store.dispatch('wipeUserDevices', userid)
-				.then(() => {
-					this.loading.wipe = false
-					this.loading.all = false
-				});
-		},
-
-		deleteUser() {
-			this.loading.delete = true;
-			this.loading.all = true;
-			let userid = this.user.id;
-			return this.$store.dispatch('deleteUser', userid)
-				.then(() => {
-					this.loading.delete = false
-					this.loading.all = false
-				});
-		},
-
-		enableDisableUser() {
-			this.loading.delete = true;
-			this.loading.all = true;
-			let userid = this.user.id;
-			let enabled = !this.user.enabled;
-			return this.$store.dispatch('enableDisableUser', {userid, enabled})
-				.then(() => {
-					this.loading.delete = false
-					this.loading.all = false
-				});
-		},
-
-		/**
-		 * Set user displayName
-		 * 
-		 * @param {string} displayName The display name
-		 * @returns {Promise}
-		 */
-		updateDisplayName() {
-			let displayName = this.$refs.displayName.value;
-			this.loading.displayName = true;
-			this.$store.dispatch('setUserData', {
-				userid: this.user.id, 
-				key: 'displayname',
-				value: displayName
-			}).then(() => {
-				this.loading.displayName = false;
-				this.$refs.displayName.value = displayName;
-			});
-		},
-
-		/**
-		 * Set user password
-		 * 
-		 * @param {string} password The email adress
-		 * @returns {Promise}
-		 */
-		updatePassword() {
-			let password = this.$refs.password.value;
-			this.loading.password = true;
-			this.$store.dispatch('setUserData', {
-				userid: this.user.id,
-				key: 'password',
-				value: password
-			}).then(() => {
-				this.loading.password = false;
-				this.$refs.password.value = ''; // empty & show placeholder 
-			});
-		},
-
-		/**
-		 * Set user mailAddress
-		 * 
-		 * @param {string} mailAddress The email adress
-		 * @returns {Promise}
-		 */
-		updateEmail() {
-			let mailAddress = this.$refs.mailAddress.value;
-			this.loading.mailAddress = true;
-			this.$store.dispatch('setUserData', {
-				userid: this.user.id,
-				key: 'email',
-				value: mailAddress
-			}).then(() => {
-				this.loading.mailAddress = false;
-				this.$refs.mailAddress.value = mailAddress;
-			});
-		},
-
-		/**
-		 * Create a new group and add user to it
-		 * 
-		 * @param {string} groups Group id
-		 * @returns {Promise}
-		 */
-		createGroup(gid) {
-			this.loading = {groups:true, subadmins:true}
-			this.$store.dispatch('addGroup', gid)
-				.then(() => {
-					this.loading = {groups:false, subadmins:false};
-					let userid = this.user.id;
-					this.$store.dispatch('addUserGroup', {userid, gid});
-				})
-				.catch(() => {
-					this.loading = {groups:false, subadmins:false};
-				});
-			return this.$store.getters.getGroups[this.groups.length];
-		},
-
-		/**
-		 * Add user to group
-		 * 
-		 * @param {object} group Group object
-		 * @returns {Promise}
-		 */
-		addUserGroup(group) {
-			if (group.canAdd === false) {
-				return false;
-			}
-			this.loading.groups = true;
-			let userid = this.user.id;
-			let gid = group.id;
-			return this.$store.dispatch('addUserGroup', {userid, gid})
-				.then(() => this.loading.groups = false);
-		},
-
-		/**
-		 * Remove user from group
-		 * 
-		 * @param {object} group Group object
-		 * @returns {Promise}
-		 */
-		removeUserGroup(group) {
-			if (group.canRemove === false) {
-				return false;
-			}
-			this.loading.groups = true;
-			let userid = this.user.id;
-			let gid = group.id;
-			return this.$store.dispatch('removeUserGroup', {userid, gid})
-				.then(() => {
-					this.loading.groups = false
-					// remove user from current list if current list is the removed group
-					if (this.$route.params.selectedGroup === gid) {
-						this.$store.commit('deleteUser', userid);
-					}
-				})
-				.catch(() => {
-					this.loading.groups = false
-				});
-		},
-
-		/**
-		 * Add user to group
-		 * 
-		 * @param {object} group Group object
-		 * @returns {Promise}
-		 */
-		addUserSubAdmin(group) {
-			this.loading.subadmins = true;
-			let userid = this.user.id;
-			let gid = group.id;
-			return this.$store.dispatch('addUserSubAdmin', {userid, gid})
-				.then(() => this.loading.subadmins = false);
-		},
-
-		/**
-		 * Remove user from group
-		 * 
-		 * @param {object} group Group object
-		 * @returns {Promise}
-		 */
-		removeUserSubAdmin(group) {
-			this.loading.subadmins = true;
-			let userid = this.user.id;
-			let gid = group.id;
-			return this.$store.dispatch('removeUserSubAdmin', {userid, gid})
-				.then(() => this.loading.subadmins = false);
-		},
-
-		/**
-		 * Dispatch quota set request
-		 * 
-		 * @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
-		 * @returns {string}
-		 */
-		setUserQuota(quota = 'none') {
-			this.loading.quota = true;
-			// ensure we only send the preset id
-			quota = quota.id ? quota.id : quota;
-			this.$store.dispatch('setUserData', {
-				userid: this.user.id, 
-				key: 'quota',
-				value: quota
-			}).then(() => this.loading.quota = false);
-			return quota;
-		},
-
-		/**
-		 * Validate quota string to make sure it's a valid human file size
-		 * 
-		 * @param {string} quota Quota in readable format '5 GB'
-		 * @returns {Promise|boolean}
-		 */
-		validateQuota(quota) {
-			// only used for new presets sent through @Tag
-			let validQuota = OC.Util.computerFileSize(quota);
-			if (validQuota !== null && validQuota >= 0) {
-				// unify format output
-				return this.setUserQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)));
-			}
-			// if no valid do not change
-			return false;
-		},
-
-		/**
-		 * Dispatch language set request
-		 * 
-		 * @param {Object} lang language object {code:'en', name:'English'}
-		 * @returns {Object}
-		 */
-		setUserLanguage(lang) {
-			this.loading.languages = true;
-			// ensure we only send the preset id
-			this.$store.dispatch('setUserData', {
-				userid: this.user.id, 
-				key: 'language',
-				value: lang.code
-			}).then(() => this.loading.languages = false);
-			return lang;
-		},
-
-		/**
-		 * Dispatch new welcome mail request
-		 */
-		sendWelcomeMail() {
-			this.loading.all = true;
-			this.$store.dispatch('sendWelcomeMail', this.user.id)
-				.then(success => {
-					if (success) {
-						// Show feedback to indicate the success
-						this.feedbackMessage = t('setting', 'Welcome mail sent!');
-						setTimeout(() => {
-							this.feedbackMessage = '';
-						}, 2000);
-					}
-					this.loading.all = false;
-				});
-		}
-
-	}
-}
-</script>

+ 4 - 3
apps/settings/src/main-admin-security.js

@@ -3,13 +3,14 @@ import Vue from 'vue'
 import AdminTwoFactor from './components/AdminTwoFactor.vue'
 import store from './store/admin-security'
 
+// eslint-disable-next-line camelcase
 __webpack_nonce__ = btoa(OC.requestToken)
 
-Vue.prototype.t = t;
+Vue.prototype.t = t
 
 // Not used here but required for legacy templates
-window.OC = window.OC || {};
-window.OC.Settings = window.OC.Settings || {};
+window.OC = window.OC || {}
+window.OC.Settings = window.OC.Settings || {}
 
 store.replaceState(
 	OCP.InitialState.loadState('settings', 'mandatory2FAState')

+ 15 - 14
apps/settings/src/main-apps-users-management.js

@@ -20,17 +20,17 @@
  *
  */
 
-import Vue from 'vue';
-import VTooltip from 'v-tooltip';
-import { sync } from 'vuex-router-sync';
+import Vue from 'vue'
+import VTooltip from 'v-tooltip'
+import { sync } from 'vuex-router-sync'
 
-import App from './App.vue';
-import router from './router';
-import store from './store';
+import App from './App.vue'
+import router from './router'
+import store from './store'
 
-Vue.use(VTooltip, { defaultHtml: false });
+Vue.use(VTooltip, { defaultHtml: false })
 
-sync(store, router);
+sync(store, router)
 
 // CSP config for webpack dynamic chunk loading
 // eslint-disable-next-line
@@ -43,15 +43,16 @@ __webpack_nonce__ = btoa(OC.requestToken)
 __webpack_public_path__ = OC.linkTo('settings', 'js/')
 
 // bind to window
-Vue.prototype.t = t;
-Vue.prototype.OC = OC;
-Vue.prototype.OCA = OCA;
-Vue.prototype.oc_userconfig = oc_userconfig;
+Vue.prototype.t = t
+Vue.prototype.OC = OC
+Vue.prototype.OCA = OCA
+// eslint-disable-next-line camelcase
+Vue.prototype.oc_userconfig = oc_userconfig
 
 const app = new Vue({
 	router,
 	store,
 	render: h => h(App)
-}).$mount('#content');
+}).$mount('#content')
 
-export { app, router, store };
+export { app, router, store }

+ 12 - 11
apps/settings/src/main-personal-security.js

@@ -19,22 +19,23 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-import Vue from 'vue';
-import VueClipboard from 'vue-clipboard2';
-import VTooltip from 'v-tooltip';
+import Vue from 'vue'
+import VueClipboard from 'vue-clipboard2'
+import VTooltip from 'v-tooltip'
 
-import AuthTokenSection from './components/AuthTokenSection';
+import AuthTokenSection from './components/AuthTokenSection'
 
-__webpack_nonce__ = btoa(OC.requestToken);
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = btoa(OC.requestToken)
 
-Vue.use(VueClipboard);
-Vue.use(VTooltip, { defaultHtml: false });
-Vue.prototype.t = t;
+Vue.use(VueClipboard)
+Vue.use(VTooltip, { defaultHtml: false })
+Vue.prototype.t = t
 
-const View = Vue.extend(AuthTokenSection);
+const View = Vue.extend(AuthTokenSection)
 new View({
 	propsData: {
 		tokens: OCP.InitialState.loadState('settings', 'app_tokens'),
-		canCreateToken: OCP.InitialState.loadState('settings', 'can_create_app_token'),
+		canCreateToken: OCP.InitialState.loadState('settings', 'can_create_app_token')
 	}
-}).$mount('#security-authtokens');
+}).$mount('#security-authtokens')

+ 6 - 6
apps/settings/src/router.js

@@ -21,14 +21,14 @@
  *
  */
 
-import Vue from 'vue';
-import Router from 'vue-router';
+import Vue from 'vue'
+import Router from 'vue-router'
 
 // Dynamic loading
-const Users = () => import('./views/Users');
-const Apps = () => import('./views/Apps');
+const Users = () => import('./views/Users')
+const Apps = () => import('./views/Apps')
 
-Vue.use(Router);
+Vue.use(Router)
 
 /*
  * This is the list of routes where the vuejs app will
@@ -80,4 +80,4 @@ export default new Router({
 			]
 		}
 	]
-});
+})

+ 10 - 24
apps/settings/src/store/admin-security.js

@@ -1,4 +1,4 @@
-/*
+/**
  * @copyright 2019 Roeland Jago Douma <roeland@famdouma.nl>
  *
  * @author 2019 Roeland Jago Douma <roeland@famdouma.nl>
@@ -24,7 +24,13 @@ import Vuex from 'vuex'
 
 Vue.use(Vuex)
 
-export const mutations = {
+const state = {
+	enforced: false,
+	enforcedGroups: [],
+	excludedGroups: []
+}
+
+const mutations = {
 	setEnforced(state, enabled) {
 		Vue.set(state, 'enforced', enabled)
 	},
@@ -36,28 +42,8 @@ export const mutations = {
 	}
 }
 
-export const actions = {
-	save ({commit}, ) {
-		commit('setEnabled', false);
-
-		return generateCodes()
-			.then(({codes, state})  => {
-			commit('setEnabled', state.enabled);
-		commit('setTotal', state.total);
-		commit('setUsed', state.used);
-		commit('setCodes', codes);
-		return true;
-	});
-	}
-}
-
 export default new Vuex.Store({
 	strict: process.env.NODE_ENV !== 'production',
-	state: {
-		enforced: false,
-		enforcedGroups: [],
-		excludedGroups: [],
-	},
-	mutations,
-	actions
+	state,
+	mutations
 })

+ 13 - 13
apps/settings/src/store/api.js

@@ -21,11 +21,11 @@
  */
 
 import axios from 'nextcloud-axios'
-import confirmPassword from 'nextcloud-password-confirmation' 
+import confirmPassword from 'nextcloud-password-confirmation'
 
 const sanitize = function(url) {
-	return url.replace(/\/$/, ''); // Remove last url slash
-};
+	return url.replace(/\/$/, '') // Remove last url slash
+}
 
 export default {
 
@@ -47,35 +47,35 @@ export default {
 	 *
 	 * Since Promise.then().catch().then() will always execute the last then
 	 * this.$store.dispatch('action').then will always be executed
-	 * 
+	 *
 	 * If you want requireAdmin failure to also catch the API request failure
 	 * you will need to throw a new error in the api.get.catch()
-	 * 
+	 *
 	 * e.g
 	 *	api.requireAdmin().then((response) => {
 	 *		api.get('url')
 	 *			.then((response) => {API success})
 	 *			.catch((error) => {throw error;});
 	 *	}).catch((error) => {requireAdmin OR API failure});
-	 * 
+	 *
 	 * @returns {Promise}
 	 */
 	requireAdmin() {
-		return confirmPassword();
+		return confirmPassword()
 	},
 	get(url) {
-		return axios.get(sanitize(url));
+		return axios.get(sanitize(url))
 	},
 	post(url, data) {
-		return axios.post(sanitize(url), data);
+		return axios.post(sanitize(url), data)
 	},
 	patch(url, data) {
-		return axios.patch(sanitize(url), data);
+		return axios.patch(sanitize(url), data)
 	},
 	put(url, data) {
-		return axios.put(sanitize(url), data);
+		return axios.put(sanitize(url), data)
 	},
 	delete(url, data) {
-		return axios.delete(sanitize(url), { data: data });
+		return axios.delete(sanitize(url), { data: data })
 	}
-};
+}

+ 130 - 130
apps/settings/src/store/apps.js

@@ -20,158 +20,158 @@
  *
  */
 
-import api from './api';
-import Vue from 'vue';
+import api from './api'
+import Vue from 'vue'
 
 const state = {
 	apps: [],
 	categories: [],
 	updateCount: 0,
 	loading: {},
-	loadingList: false,
-};
+	loadingList: false
+}
 
 const mutations = {
 
 	APPS_API_FAILURE(state, error) {
-		OC.Notification.showHtml(t('settings','An error occured during the request. Unable to proceed.')+'<br>'+error.error.response.data.data.message, {timeout: 7});
-		console.log(state, error);
+		OC.Notification.showHtml(t('settings', 'An error occured during the request. Unable to proceed.') + '<br>' + error.error.response.data.data.message, { timeout: 7 })
+		console.error(state, error)
 	},
 
-	initCategories(state, {categories, updateCount}) {
-		state.categories = categories;
-		state.updateCount = updateCount;
+	initCategories(state, { categories, updateCount }) {
+		state.categories = categories
+		state.updateCount = updateCount
 	},
 
 	setUpdateCount(state, updateCount) {
-		state.updateCount = updateCount;
+		state.updateCount = updateCount
 	},
 
 	addCategory(state, category) {
-		state.categories.push(category);
+		state.categories.push(category)
 	},
 
 	appendCategories(state, categoriesArray) {
 		// convert obj to array
-		state.categories = categoriesArray;
+		state.categories = categoriesArray
 	},
 
 	setAllApps(state, apps) {
-		state.apps = apps;
+		state.apps = apps
 	},
 
-	setError(state, {appId, error}) {
+	setError(state, { appId, error }) {
 		if (!Array.isArray(appId)) {
-			appId = [appId];
+			appId = [appId]
 		}
 		appId.forEach((_id) => {
-			let app = state.apps.find(app => app.id === _id);
-			app.error = error;
-		});
+			let app = state.apps.find(app => app.id === _id)
+			app.error = error
+		})
 	},
 
-	clearError(state, {appId, error}) {
-		let app = state.apps.find(app => app.id === appId);
-		app.error = null;
+	clearError(state, { appId, error }) {
+		let app = state.apps.find(app => app.id === appId)
+		app.error = null
 	},
 
-	enableApp(state, {appId, groups}) {
-		let app = state.apps.find(app => app.id === appId);
-		app.active = true;
-		app.groups = groups;
+	enableApp(state, { appId, groups }) {
+		let app = state.apps.find(app => app.id === appId)
+		app.active = true
+		app.groups = groups
 	},
 
 	disableApp(state, appId) {
-		let app = state.apps.find(app => app.id === appId);
-		app.active = false;
-		app.groups = [];
+		let app = state.apps.find(app => app.id === appId)
+		app.active = false
+		app.groups = []
 		if (app.removable) {
-			app.canUnInstall = true;
+			app.canUnInstall = true
 		}
 	},
 
 	uninstallApp(state, appId) {
-		state.apps.find(app => app.id === appId).active = false;
-		state.apps.find(app => app.id === appId).groups = [];
-		state.apps.find(app => app.id === appId).needsDownload = true;
-		state.apps.find(app => app.id === appId).installed = false;
-		state.apps.find(app => app.id === appId).canUnInstall = false;
-		state.apps.find(app => app.id === appId).canInstall = true;
+		state.apps.find(app => app.id === appId).active = false
+		state.apps.find(app => app.id === appId).groups = []
+		state.apps.find(app => app.id === appId).needsDownload = true
+		state.apps.find(app => app.id === appId).installed = false
+		state.apps.find(app => app.id === appId).canUnInstall = false
+		state.apps.find(app => app.id === appId).canInstall = true
 	},
 
 	updateApp(state, appId) {
-		let app = state.apps.find(app => app.id === appId);
-		let version = app.update;
-		app.update = null;
-		app.version = version;
-		state.updateCount--;
+		let app = state.apps.find(app => app.id === appId)
+		let version = app.update
+		app.update = null
+		app.version = version
+		state.updateCount--
 
 	},
 
 	resetApps(state) {
-		state.apps = [];
+		state.apps = []
 	},
 	reset(state) {
-		state.apps = [];
-		state.categories = [];
-		state.updateCount = 0;
+		state.apps = []
+		state.categories = []
+		state.updateCount = 0
 	},
 	startLoading(state, id) {
 		if (Array.isArray(id)) {
 			id.forEach((_id) => {
-				Vue.set(state.loading, _id, true);
+				Vue.set(state.loading, _id, true)
 			})
 		} else {
-			Vue.set(state.loading, id, true);
+			Vue.set(state.loading, id, true)
 		}
 	},
 	stopLoading(state, id) {
 		if (Array.isArray(id)) {
 			id.forEach((_id) => {
-				Vue.set(state.loading, _id, false);
+				Vue.set(state.loading, _id, false)
 			})
 		} else {
-			Vue.set(state.loading, id, false);
+			Vue.set(state.loading, id, false)
 		}
-	},
-};
+	}
+}
 
 const getters = {
 	loading(state) {
 		return function(id) {
-			return state.loading[id];
+			return state.loading[id]
 		}
 	},
 	getCategories(state) {
-		return state.categories;
+		return state.categories
 	},
 	getAllApps(state) {
-		return state.apps;
+		return state.apps
 	},
 	getUpdateCount(state) {
-		return state.updateCount;
+		return state.updateCount
 	}
-};
+}
 
 const actions = {
 
 	enableApp(context, { appId, groups }) {
-		let apps;
+		let apps
 		if (Array.isArray(appId)) {
-			apps = appId;
+			apps = appId
 		} else {
-			apps = [appId];
+			apps = [appId]
 		}
 		return api.requireAdmin().then((response) => {
-			context.commit('startLoading', apps);
-			context.commit('startLoading', 'install');
-			return api.post(OC.generateUrl(`settings/apps/enable`), {appIds: apps, groups: groups})
+			context.commit('startLoading', apps)
+			context.commit('startLoading', 'install')
+			return api.post(OC.generateUrl(`settings/apps/enable`), { appIds: apps, groups: groups })
 				.then((response) => {
-					context.commit('stopLoading', apps);
-					context.commit('stopLoading', 'install');
+					context.commit('stopLoading', apps)
+					context.commit('stopLoading', 'install')
 					apps.forEach(_appId => {
-						context.commit('enableApp', {appId: _appId, groups: groups});
-					});
+						context.commit('enableApp', { appId: _appId, groups: groups })
+					})
 
 					// check for server health
 					return api.get(OC.generateUrl('apps/files'))
@@ -182,146 +182,146 @@ const actions = {
 										'settings',
 										'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.'
 									),
-									t('settings','App update'),
-									function () {
-										window.location.reload();
+									t('settings', 'App update'),
+									function() {
+										window.location.reload()
 									},
 									true
-								);
+								)
 								setTimeout(function() {
-									location.reload();
-								}, 5000);
+									location.reload()
+								}, 5000)
 							}
 						})
-						.catch((error) => {
+						.catch(() => {
 							if (!Array.isArray(appId)) {
 								context.commit('setError', {
 									appId: apps,
 									error: t('settings', 'Error: This app can not be enabled because it makes the server unstable')
-								});
+								})
 							}
-						});
+						})
 				})
 				.catch((error) => {
-					context.commit('stopLoading', apps);
-					context.commit('stopLoading', 'install');
+					context.commit('stopLoading', apps)
+					context.commit('stopLoading', 'install')
 					context.commit('setError', {
 						appId: apps,
 						error: error.response.data.data.message
-					});
-					context.commit('APPS_API_FAILURE', { appId, error});
+					})
+					context.commit('APPS_API_FAILURE', { appId, error })
 				})
-		}).catch((error) => context.commit('API_FAILURE', { appId, error }));
+		}).catch((error) => context.commit('API_FAILURE', { appId, error }))
 	},
 	forceEnableApp(context, { appId, groups }) {
-		let apps;
+		let apps
 		if (Array.isArray(appId)) {
-			apps = appId;
+			apps = appId
 		} else {
-			apps = [appId];
+			apps = [appId]
 		}
 		return api.requireAdmin().then(() => {
-			context.commit('startLoading', apps);
-			context.commit('startLoading', 'install');
-			return api.post(OC.generateUrl(`settings/apps/force`), {appId})
+			context.commit('startLoading', apps)
+			context.commit('startLoading', 'install')
+			return api.post(OC.generateUrl(`settings/apps/force`), { appId })
 				.then((response) => {
 					// TODO: find a cleaner solution
-					location.reload();
+					location.reload()
 				})
 				.catch((error) => {
-					context.commit('stopLoading', apps);
-					context.commit('stopLoading', 'install');
+					context.commit('stopLoading', apps)
+					context.commit('stopLoading', 'install')
 					context.commit('setError', {
 						appId: apps,
 						error: error.response.data.data.message
-					});
-					context.commit('APPS_API_FAILURE', { appId, error});
+					})
+					context.commit('APPS_API_FAILURE', { appId, error })
 				})
-		}).catch((error) => context.commit('API_FAILURE', { appId, error }));
+		}).catch((error) => context.commit('API_FAILURE', { appId, error }))
 	},
 	disableApp(context, { appId }) {
-		let apps;
+		let apps
 		if (Array.isArray(appId)) {
-			apps = appId;
+			apps = appId
 		} else {
-			apps = [appId];
+			apps = [appId]
 		}
 		return api.requireAdmin().then((response) => {
-			context.commit('startLoading', apps);
-			return api.post(OC.generateUrl(`settings/apps/disable`), {appIds: apps})
+			context.commit('startLoading', apps)
+			return api.post(OC.generateUrl(`settings/apps/disable`), { appIds: apps })
 				.then((response) => {
-					context.commit('stopLoading', apps);
+					context.commit('stopLoading', apps)
 					apps.forEach(_appId => {
-						context.commit('disableApp', _appId);
-					});
-					return true;
+						context.commit('disableApp', _appId)
+					})
+					return true
 				})
 				.catch((error) => {
-					context.commit('stopLoading', apps);
+					context.commit('stopLoading', apps)
 					context.commit('APPS_API_FAILURE', { appId, error })
 				})
-		}).catch((error) => context.commit('API_FAILURE', { appId, error }));
+		}).catch((error) => context.commit('API_FAILURE', { appId, error }))
 	},
 	uninstallApp(context, { appId }) {
 		return api.requireAdmin().then((response) => {
-			context.commit('startLoading', appId);
+			context.commit('startLoading', appId)
 			return api.get(OC.generateUrl(`settings/apps/uninstall/${appId}`))
 				.then((response) => {
-					context.commit('stopLoading', appId);
-					context.commit('uninstallApp', appId);
-					return true;
+					context.commit('stopLoading', appId)
+					context.commit('uninstallApp', appId)
+					return true
 				})
 				.catch((error) => {
-					context.commit('stopLoading', appId);
+					context.commit('stopLoading', appId)
 					context.commit('APPS_API_FAILURE', { appId, error })
 				})
-		}).catch((error) => context.commit('API_FAILURE', { appId, error }));
+		}).catch((error) => context.commit('API_FAILURE', { appId, error }))
 	},
 
 	updateApp(context, { appId }) {
 		return api.requireAdmin().then((response) => {
-			context.commit('startLoading', appId);
-			context.commit('startLoading', 'install');
+			context.commit('startLoading', appId)
+			context.commit('startLoading', 'install')
 			return api.get(OC.generateUrl(`settings/apps/update/${appId}`))
 				.then((response) => {
-					context.commit('stopLoading', 'install');
-					context.commit('stopLoading', appId);
-					context.commit('updateApp', appId);
-					return true;
+					context.commit('stopLoading', 'install')
+					context.commit('stopLoading', appId)
+					context.commit('updateApp', appId)
+					return true
 				})
 				.catch((error) => {
-					context.commit('stopLoading', appId);
-					context.commit('stopLoading', 'install');
+					context.commit('stopLoading', appId)
+					context.commit('stopLoading', 'install')
 					context.commit('APPS_API_FAILURE', { appId, error })
 				})
-		}).catch((error) => context.commit('API_FAILURE', { appId, error }));
+		}).catch((error) => context.commit('API_FAILURE', { appId, error }))
 	},
 
 	getAllApps(context) {
-		context.commit('startLoading', 'list');
+		context.commit('startLoading', 'list')
 		return api.get(OC.generateUrl(`settings/apps/list`))
 			.then((response) => {
-				context.commit('setAllApps', response.data.apps);
-				context.commit('stopLoading', 'list');
-				return true;
+				context.commit('setAllApps', response.data.apps)
+				context.commit('stopLoading', 'list')
+				return true
 			})
 			.catch((error) => context.commit('API_FAILURE', error))
 	},
 
 	getCategories(context) {
-		context.commit('startLoading', 'categories');
+		context.commit('startLoading', 'categories')
 		return api.get(OC.generateUrl('settings/apps/categories'))
 			.then((response) => {
 				if (response.data.length > 0) {
-					context.commit('appendCategories', response.data);
-					context.commit('stopLoading', 'categories');
-					return true;
+					context.commit('appendCategories', response.data)
+					context.commit('stopLoading', 'categories')
+					return true
 				}
-				return false;
+				return false
 			})
-			.catch((error) => context.commit('API_FAILURE', error));
-	},
+			.catch((error) => context.commit('API_FAILURE', error))
+	}
 
-};
+}
 
-export default { state, mutations, getters, actions };
+export default { state, mutations, getters, actions }

+ 15 - 15
apps/settings/src/store/index.js

@@ -1,4 +1,4 @@
-/*
+/**
  * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  *
  * @author John Molakvoæ <skjnldsv@protonmail.com>
@@ -21,28 +21,28 @@
  *
  */
 
-import Vue from 'vue';
-import Vuex from 'vuex';
-import users from './users';
-import apps from './apps';
-import settings from './settings';
-import oc from './oc';
+import Vue from 'vue'
+import Vuex from 'vuex'
+import users from './users'
+import apps from './apps'
+import settings from './settings'
+import oc from './oc'
 
 Vue.use(Vuex)
 
-const debug = process.env.NODE_ENV !== 'production';
+const debug = process.env.NODE_ENV !== 'production'
 
 const mutations = {
 	API_FAILURE(state, error) {
 		try {
-			let message = error.error.response.data.ocs.meta.message;
-			OC.Notification.showHtml(t('settings','An error occured during the request. Unable to proceed.')+'<br>'+message, {timeout: 7});
-		} catch(e) {
-			OC.Notification.showTemporary(t('settings','An error occured during the request. Unable to proceed.'));
+			let message = error.error.response.data.ocs.meta.message
+			OC.Notification.showHtml(t('settings', 'An error occured during the request. Unable to proceed.') + '<br>' + message, { timeout: 7 })
+		} catch (e) {
+			OC.Notification.showTemporary(t('settings', 'An error occured during the request. Unable to proceed.'))
 		}
-		console.log(state, error);
+		console.error(state, error)
 	}
-};
+}
 
 export default new Vuex.Store({
 	modules: {
@@ -54,4 +54,4 @@ export default new Vuex.Store({
 	strict: debug,
 
 	mutations
-});
+})

+ 15 - 15
apps/settings/src/store/oc.js

@@ -1,4 +1,4 @@
-/*
+/**
  * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  *
  * @author John Molakvoæ <skjnldsv@protonmail.com>
@@ -20,28 +20,28 @@
  *
  */
 
-import api from './api';
+import api from './api'
 
-const state = {};
-const mutations = {};
-const getters = {};
+const state = {}
+const mutations = {}
+const getters = {}
 const actions = {
 	/**
      * Set application config in database
-     * 
-	 * @param {Object} context
-     * @param {Object} options
+     *
+	 * @param {Object} context store context
+     * @param {Object} options destructuring object
 	 * @param {string} options.app Application name
 	 * @param {boolean} options.key Config key
 	 * @param {boolean} options.value Value to set
 	 * @returns{Promise}
 	 */
-	setAppConfig(context, {app, key, value}) {
+	setAppConfig(context, { app, key, value }) {
 		return api.requireAdmin().then((response) => {
-			return api.post(OC.linkToOCS(`apps/provisioning_api/api/v1/config/apps/${app}/${key}`, 2), {value: value})
-				.catch((error) => {throw error;});
-		}).catch((error) => context.commit('API_FAILURE', { app, key, value, error }));;
-    }
-};
+			return api.post(OC.linkToOCS(`apps/provisioning_api/api/v1/config/apps/${app}/${key}`, 2), { value: value })
+				.catch((error) => { throw error })
+		}).catch((error) => context.commit('API_FAILURE', { app, key, value, error }))
+	}
+}
 
-export default {state, mutations, getters, actions};
+export default { state, mutations, getters, actions }

+ 8 - 10
apps/settings/src/store/settings.js

@@ -1,4 +1,4 @@
-/*
+/**
  * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  *
  * @author John Molakvoæ <skjnldsv@protonmail.com>
@@ -20,21 +20,19 @@
  *
  */
 
-import api from './api';
-
 const state = {
 	serverData: {}
-};
+}
 const mutations = {
 	setServerData(state, data) {
-		state.serverData = data;
+		state.serverData = data
 	}
-};
+}
 const getters = {
 	getServerData(state) {
-		return state.serverData;
+		return state.serverData
 	}
-};
-const actions = {};
+}
+const actions = {}
 
-export default {state, mutations, getters, actions};
+export default { state, mutations, getters, actions }

Some files were not shown because too many files changed in this diff