summaryrefslogtreecommitdiffstats
path: root/apps/settings/src
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2019-09-25 18:19:42 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2019-10-01 17:16:09 +0200
commitb9bc2417e7a8dc81feb0abe20359bedaf864f790 (patch)
tree61b47fbf37c1d168da8625224debde9e6a985348 /apps/settings/src
parent7fb651235128dcbca8a6683b5cdafdf835f46300 (diff)
downloadnextcloud-server-b9bc2417e7a8dc81feb0abe20359bedaf864f790.tar.gz
nextcloud-server-b9bc2417e7a8dc81feb0abe20359bedaf864f790.zip
Comply to eslint
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/settings/src')
-rw-r--r--apps/settings/src/App.vue6
-rw-r--r--apps/settings/src/components/AdminTwoFactor.vue209
-rw-r--r--apps/settings/src/components/AppDetails.vue326
-rw-r--r--apps/settings/src/components/AppList.vue201
-rw-r--r--apps/settings/src/components/AppList/AppItem.vue179
-rw-r--r--apps/settings/src/components/AppList/AppScore.vue38
-rw-r--r--apps/settings/src/components/AppManagement.vue138
-rw-r--r--apps/settings/src/components/AuthToken.vue128
-rw-r--r--apps/settings/src/components/AuthTokenList.vue96
-rw-r--r--apps/settings/src/components/AuthTokenSection.vue272
-rw-r--r--apps/settings/src/components/AuthTokenSetupDialogue.vue227
-rw-r--r--apps/settings/src/components/PrefixMixin.vue32
-rw-r--r--apps/settings/src/components/SvgFilterMixin.vue40
-rw-r--r--apps/settings/src/components/UserList.vue553
-rw-r--r--apps/settings/src/components/appList.vue143
-rw-r--r--apps/settings/src/components/appList/appScore.vue24
-rw-r--r--apps/settings/src/components/prefixMixin.vue14
-rw-r--r--apps/settings/src/components/svgFilterMixin.vue32
-rw-r--r--apps/settings/src/components/userList/UserRow.vue706
-rw-r--r--apps/settings/src/components/userList/userRow.vue574
-rw-r--r--apps/settings/src/main-admin-security.js7
-rw-r--r--apps/settings/src/main-apps-users-management.js29
-rw-r--r--apps/settings/src/main-personal-security.js23
-rw-r--r--apps/settings/src/router.js12
-rw-r--r--apps/settings/src/store/admin-security.js34
-rw-r--r--apps/settings/src/store/api.js26
-rw-r--r--apps/settings/src/store/apps.js260
-rw-r--r--apps/settings/src/store/index.js30
-rw-r--r--apps/settings/src/store/oc.js30
-rw-r--r--apps/settings/src/store/settings.js18
-rw-r--r--apps/settings/src/store/users.js376
-rw-r--r--apps/settings/src/views/Apps.vue167
-rw-r--r--apps/settings/src/views/Users.vue465
33 files changed, 3541 insertions, 1874 deletions
diff --git a/apps/settings/src/App.vue b/apps/settings/src/App.vue
index c7c32ba9d63..591a784c02a 100644
--- a/apps/settings/src/App.vue
+++ b/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))
}
}
}
diff --git a/apps/settings/src/components/AdminTwoFactor.vue b/apps/settings/src/components/AdminTwoFactor.vue
index a1f28f5cfdd..934b77928dc 100644
--- a/apps/settings/src/components/AdminTwoFactor.vue
+++ b/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>
diff --git a/apps/settings/src/components/AppDetails.vue b/apps/settings/src/components/AppDetails.vue
new file mode 100644
index 00000000000..b4af8f51e4d
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/AppList.vue b/apps/settings/src/components/AppList.vue
new file mode 100644
index 00000000000..3e40eeb5fbb
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue
new file mode 100644
index 00000000000..6b66163cfed
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/AppList/AppScore.vue b/apps/settings/src/components/AppList/AppScore.vue
new file mode 100644
index 00000000000..810a8b54e8f
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/AppManagement.vue b/apps/settings/src/components/AppManagement.vue
new file mode 100644
index 00000000000..4de40728c5d
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/AuthToken.vue b/apps/settings/src/components/AuthToken.vue
index fb5a331b72e..034efc4b6a8 100644
--- a/apps/settings/src/components/AuthToken.vue
+++ b/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)
}
}
}
diff --git a/apps/settings/src/components/AuthTokenList.vue b/apps/settings/src/components/AuthTokenList.vue
index a7c0faf1b7b..65f42197eda 100644
--- a/apps/settings/src/components/AuthTokenList.vue
+++ b/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>
diff --git a/apps/settings/src/components/AuthTokenSection.vue b/apps/settings/src/components/AuthTokenSection.vue
index c53256102d3..fe7ec640afb 100644
--- a/apps/settings/src/components/AuthTokenSection.vue
+++ b/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>
diff --git a/apps/settings/src/components/AuthTokenSetupDialogue.vue b/apps/settings/src/components/AuthTokenSetupDialogue.vue
index 000e873e659..22c867b3c96 100644
--- a/apps/settings/src/components/AuthTokenSetupDialogue.vue
+++ b/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>
diff --git a/apps/settings/src/components/PrefixMixin.vue b/apps/settings/src/components/PrefixMixin.vue
new file mode 100644
index 00000000000..e43063d25b0
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/SvgFilterMixin.vue b/apps/settings/src/components/SvgFilterMixin.vue
new file mode 100644
index 00000000000..eab5bee973b
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/UserList.vue b/apps/settings/src/components/UserList.vue
new file mode 100644
index 00000000000..f832837f910
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/appList.vue b/apps/settings/src/components/appList.vue
index dd693b2fdce..3e40eeb5fbb 100644
--- a/apps/settings/src/components/appList.vue
+++ b/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>
diff --git a/apps/settings/src/components/appList/appScore.vue b/apps/settings/src/components/appList/appScore.vue
index bf04c688186..810a8b54e8f 100644
--- a/apps/settings/src/components/appList/appScore.vue
+++ b/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> \ No newline at end of file
+ }
+}
+</script>
diff --git a/apps/settings/src/components/prefixMixin.vue b/apps/settings/src/components/prefixMixin.vue
index e2feb63276d..e43063d25b0 100644
--- a/apps/settings/src/components/prefixMixin.vue
+++ b/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> \ No newline at end of file
+}
+</script>
diff --git a/apps/settings/src/components/svgFilterMixin.vue b/apps/settings/src/components/svgFilterMixin.vue
index 1d6e83d4829..eab5bee973b 100644
--- a/apps/settings/src/components/svgFilterMixin.vue
+++ b/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> \ No newline at end of file
+}
+</script>
diff --git a/apps/settings/src/components/userList/UserRow.vue b/apps/settings/src/components/userList/UserRow.vue
new file mode 100644
index 00000000000..c27523dfadf
--- /dev/null
+++ b/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>
diff --git a/apps/settings/src/components/userList/userRow.vue b/apps/settings/src/components/userList/userRow.vue
deleted file mode 100644
index 4bcc40965b0..00000000000
--- a/apps/settings/src/components/userList/userRow.vue
+++ /dev/null
@@ -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>
diff --git a/apps/settings/src/main-admin-security.js b/apps/settings/src/main-admin-security.js
index a728c085b43..d775f6ac133 100644
--- a/apps/settings/src/main-admin-security.js
+++ b/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')
diff --git a/apps/settings/src/main-apps-users-management.js b/apps/settings/src/main-apps-users-management.js
index a8627c2277d..e7d5b4e91c3 100644
--- a/apps/settings/src/main-apps-users-management.js
+++ b/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 }
diff --git a/apps/settings/src/main-personal-security.js b/apps/settings/src/main-personal-security.js
index 9f020efd5f4..dd20d5d1031 100644
--- a/apps/settings/src/main-personal-security.js
+++ b/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')
diff --git a/apps/settings/src/router.js b/apps/settings/src/router.js
index 512ab813840..7612fdea895 100644
--- a/apps/settings/src/router.js
+++ b/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({
]
}
]
-});
+})
diff --git a/apps/settings/src/store/admin-security.js b/apps/settings/src/store/admin-security.js
index 997aab2af58..e86783557f2 100644
--- a/apps/settings/src/store/admin-security.js
+++ b/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
})
diff --git a/apps/settings/src/store/api.js b/apps/settings/src/store/api.js
index 185e80253a3..98959de7bf5 100644
--- a/apps/settings/src/store/api.js
+++ b/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 })
}
-}; \ No newline at end of file
+}
diff --git a/apps/settings/src/store/apps.js b/apps/settings/src/store/apps.js
index 8074eac2e00..47698382904 100644
--- a/apps/settings/src/store/apps.js
+++ b/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 }; \ No newline at end of file
+export default { state, mutations, getters, actions }
diff --git a/apps/settings/src/store/index.js b/apps/settings/src/store/index.js
index 00bcd67db39..c640d32cd6a 100644
--- a/apps/settings/src/store/index.js
+++ b/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
-});
+})
diff --git a/apps/settings/src/store/oc.js b/apps/settings/src/store/oc.js
index afa13fe6b18..ff14f8ae7e3 100644
--- a/apps/settings/src/store/oc.js
+++ b/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 }
diff --git a/apps/settings/src/store/settings.js b/apps/settings/src/store/settings.js
index 5f0bcfa60aa..88086910c39 100644
--- a/apps/settings/src/store/settings.js
+++ b/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 }
diff --git a/apps/settings/src/store/users.js b/apps/settings/src/store/users.js
index 1b174b21bf4..e2eb94610af 100644
--- a/apps/settings/src/store/users.js
+++ b/apps/settings/src/store/users.js
@@ -1,4 +1,4 @@
-/*
+/**
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
@@ -20,7 +20,7 @@
*
*/
-import api from './api';
+import api from './api'
const orderGroups = function(groups, orderBy) {
/* const SORT_USERCOUNT = 1;
@@ -28,11 +28,11 @@ const orderGroups = function(groups, orderBy) {
* https://github.com/nextcloud/server/blob/208e38e84e1a07a49699aa90dc5b7272d24489f0/lib/private/Group/MetaData.php#L34
*/
if (orderBy === 1) {
- return groups.sort((a, b) => a.usercount-a.disabled < b.usercount - b.disabled);
+ return groups.sort((a, b) => a.usercount - a.disabled < b.usercount - b.disabled)
} else {
- return groups.sort((a, b) => a.name.localeCompare(b.name));
+ return groups.sort((a, b) => a.name.localeCompare(b.name))
}
-};
+}
const defaults = {
group: {
@@ -43,7 +43,7 @@ const defaults = {
canAdd: true,
canRemove: true
}
-};
+}
const state = {
users: [],
@@ -53,145 +53,146 @@ const state = {
usersOffset: 0,
usersLimit: 25,
userCount: 0
-};
+}
const mutations = {
appendUsers(state, usersObj) {
// convert obj to array
- let users = state.users.concat(Object.keys(usersObj).map(userid => usersObj[userid]));
- state.usersOffset += state.usersLimit;
- state.users = users;
+ let users = state.users.concat(Object.keys(usersObj).map(userid => usersObj[userid]))
+ state.usersOffset += state.usersLimit
+ state.users = users
},
setPasswordPolicyMinLength(state, length) {
- state.minPasswordLength = length!=='' ? length : 0;
+ state.minPasswordLength = length !== '' ? length : 0
},
- initGroups(state, {groups, orderBy, userCount}) {
- state.groups = groups.map(group => Object.assign({}, defaults.group, group));
- state.orderBy = orderBy;
- state.userCount = userCount;
- state.groups = orderGroups(state.groups, state.orderBy);
-
+ initGroups(state, { groups, orderBy, userCount }) {
+ state.groups = groups.map(group => Object.assign({}, defaults.group, group))
+ state.orderBy = orderBy
+ state.userCount = userCount
+ state.groups = orderGroups(state.groups, state.orderBy)
+
},
- addGroup(state, {gid, displayName}) {
+ addGroup(state, { gid, displayName }) {
try {
if (typeof state.groups.find((group) => group.id === gid) !== 'undefined') {
- return;
+ return
}
// extend group to default values
let group = Object.assign({}, defaults.group, {
id: gid,
- name: displayName,
- });
- state.groups.push(group);
- state.groups = orderGroups(state.groups, state.orderBy);
+ name: displayName
+ })
+ state.groups.push(group)
+ state.groups = orderGroups(state.groups, state.orderBy)
} catch (e) {
- console.log('Can\'t create group', e);
+ console.error('Can\'t create group', e)
}
},
removeGroup(state, gid) {
- let groupIndex = state.groups.findIndex(groupSearch => groupSearch.id == gid);
+ let groupIndex = state.groups.findIndex(groupSearch => groupSearch.id === gid)
if (groupIndex >= 0) {
- state.groups.splice(groupIndex, 1);
+ state.groups.splice(groupIndex, 1)
}
},
addUserGroup(state, { userid, gid }) {
- let group = state.groups.find(groupSearch => groupSearch.id == gid);
- let user = state.users.find(user => user.id == userid);
+ let group = state.groups.find(groupSearch => groupSearch.id === gid)
+ let user = state.users.find(user => user.id === userid)
// increase count if user is enabled
if (group && user.enabled) {
- group.usercount++;
+ group.usercount++
}
- let groups = user.groups;
- groups.push(gid);
- state.groups = orderGroups(state.groups, state.orderBy);
+ let groups = user.groups
+ groups.push(gid)
+ state.groups = orderGroups(state.groups, state.orderBy)
},
removeUserGroup(state, { userid, gid }) {
- let group = state.groups.find(groupSearch => groupSearch.id == gid);
- let user = state.users.find(user => user.id == userid);
+ let group = state.groups.find(groupSearch => groupSearch.id === gid)
+ let user = state.users.find(user => user.id === userid)
// lower count if user is enabled
if (group && user.enabled) {
- group.usercount--;
+ group.usercount--
}
- let groups = user.groups;
- groups.splice(groups.indexOf(gid),1);
- state.groups = orderGroups(state.groups, state.orderBy);
+ let groups = user.groups
+ groups.splice(groups.indexOf(gid), 1)
+ state.groups = orderGroups(state.groups, state.orderBy)
},
addUserSubAdmin(state, { userid, gid }) {
- let groups = state.users.find(user => user.id == userid).subadmin;
- groups.push(gid);
+ let groups = state.users.find(user => user.id === userid).subadmin
+ groups.push(gid)
},
removeUserSubAdmin(state, { userid, gid }) {
- let groups = state.users.find(user => user.id == userid).subadmin;
- groups.splice(groups.indexOf(gid),1);
+ let groups = state.users.find(user => user.id === userid).subadmin
+ groups.splice(groups.indexOf(gid), 1)
},
deleteUser(state, userid) {
- let userIndex = state.users.findIndex(user => user.id == userid);
- state.users.splice(userIndex, 1);
+ let userIndex = state.users.findIndex(user => user.id === userid)
+ state.users.splice(userIndex, 1)
},
addUserData(state, response) {
- state.users.push(response.data.ocs.data);
+ state.users.push(response.data.ocs.data)
},
enableDisableUser(state, { userid, enabled }) {
- let user = state.users.find(user => user.id == userid);
- user.enabled = enabled;
+ let user = state.users.find(user => user.id === userid)
+ user.enabled = enabled
// increment or not
- state.groups.find(group => group.id == 'disabled').usercount += enabled ? -1 : 1;
- state.userCount += enabled ? 1 : -1;
+ state.groups.find(group => group.id === 'disabled').usercount += enabled ? -1 : 1
+ state.userCount += enabled ? 1 : -1
user.groups.forEach(group => {
// Increment disabled count
- state.groups.find(groupSearch => groupSearch.id == group).disabled += enabled ? -1 : 1;
- });
+ state.groups.find(groupSearch => groupSearch.id === group).disabled += enabled ? -1 : 1
+ })
},
setUserData(state, { userid, key, value }) {
if (key === 'quota') {
- let humanValue = OC.Util.computerFileSize(value);
- state.users.find(user => user.id == userid)[key][key] = humanValue!==null ? humanValue : value;
+ let humanValue = OC.Util.computerFileSize(value)
+ state.users.find(user => user.id === userid)[key][key] = humanValue !== null ? humanValue : value
} else {
- state.users.find(user => user.id == userid)[key] = value;
+ state.users.find(user => user.id === userid)[key] = value
}
},
/**
* Reset users list
+ * @param {Object} state the store state
*/
resetUsers(state) {
- state.users = [];
- state.usersOffset = 0;
+ state.users = []
+ state.usersOffset = 0
}
-};
+}
const getters = {
getUsers(state) {
- return state.users;
+ return state.users
},
getGroups(state) {
- return state.groups;
+ return state.groups
},
getSubadminGroups(state) {
// Can't be subadmin of admin or disabled
- return state.groups.filter(group => group.id !== 'admin' && group.id !== 'disabled');
+ return state.groups.filter(group => group.id !== 'admin' && group.id !== 'disabled')
},
getPasswordPolicyMinLength(state) {
- return state.minPasswordLength;
+ return state.minPasswordLength
},
getUsersOffset(state) {
- return state.usersOffset;
+ return state.usersOffset
},
getUsersLimit(state) {
- return state.usersLimit;
+ return state.usersLimit
},
getUserCount(state) {
- return state.userCount;
+ return state.userCount
}
-};
+}
const actions = {
/**
* Get all users with full details
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {int} options.offset List offset to request
* @param {int} options.limit List number to return from offset
* @param {string} options.search Search amongst users
@@ -199,74 +200,74 @@ const actions = {
* @returns {Promise}
*/
getUsers(context, { offset, limit, search, group }) {
- search = typeof search === 'string' ? search : '';
- group = typeof group === 'string' ? group : '';
+ search = typeof search === 'string' ? search : ''
+ group = typeof group === 'string' ? group : ''
if (group !== '') {
return api.get(OC.linkToOCS(`cloud/groups/${group}/users/details?offset=${offset}&limit=${limit}&search=${search}`, 2))
- .then((response) => {
- if (Object.keys(response.data.ocs.data.users).length > 0) {
- context.commit('appendUsers', response.data.ocs.data.users);
- return true;
- }
- return false;
- })
- .catch((error) => context.commit('API_FAILURE', error));
+ .then((response) => {
+ if (Object.keys(response.data.ocs.data.users).length > 0) {
+ context.commit('appendUsers', response.data.ocs.data.users)
+ return true
+ }
+ return false
+ })
+ .catch((error) => context.commit('API_FAILURE', error))
}
return api.get(OC.linkToOCS(`cloud/users/details?offset=${offset}&limit=${limit}&search=${search}`, 2))
.then((response) => {
if (Object.keys(response.data.ocs.data.users).length > 0) {
- context.commit('appendUsers', response.data.ocs.data.users);
- return true;
+ context.commit('appendUsers', response.data.ocs.data.users)
+ return true
}
- return false;
+ return false
})
- .catch((error) => context.commit('API_FAILURE', error));
+ .catch((error) => context.commit('API_FAILURE', error))
},
getGroups(context, { offset, limit, search }) {
- search = typeof search === 'string' ? search : '';
- let limitParam = limit === -1 ? '' : `&limit=${limit}`;
+ search = typeof search === 'string' ? search : ''
+ let limitParam = limit === -1 ? '' : `&limit=${limit}`
return api.get(OC.linkToOCS(`cloud/groups?offset=${offset}&search=${search}${limitParam}`, 2))
.then((response) => {
if (Object.keys(response.data.ocs.data.groups).length > 0) {
response.data.ocs.data.groups.forEach(function(group) {
- context.commit('addGroup', {gid: group, displayName: group});
- });
- return true;
+ context.commit('addGroup', { gid: group, displayName: group })
+ })
+ return true
}
- return false;
+ return false
})
- .catch((error) => context.commit('API_FAILURE', error));
+ .catch((error) => context.commit('API_FAILURE', error))
},
/**
* Get all users with full details
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {int} options.offset List offset to request
* @param {int} options.limit List number to return from offset
* @returns {Promise}
*/
getUsersFromList(context, { offset, limit, search }) {
- search = typeof search === 'string' ? search : '';
+ search = typeof search === 'string' ? search : ''
return api.get(OC.linkToOCS(`cloud/users/details?offset=${offset}&limit=${limit}&search=${search}`, 2))
.then((response) => {
if (Object.keys(response.data.ocs.data.users).length > 0) {
- context.commit('appendUsers', response.data.ocs.data.users);
- return true;
+ context.commit('appendUsers', response.data.ocs.data.users)
+ return true
}
- return false;
+ return false
})
- .catch((error) => context.commit('API_FAILURE', error));
+ .catch((error) => context.commit('API_FAILURE', error))
},
/**
* Get all users with full details from a groupid
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {int} options.offset List offset to request
* @param {int} options.limit List number to return from offset
* @returns {Promise}
@@ -274,45 +275,44 @@ const actions = {
getUsersFromGroup(context, { groupid, offset, limit }) {
return api.get(OC.linkToOCS(`cloud/users/${groupid}/details?offset=${offset}&limit=${limit}`, 2))
.then((response) => context.commit('getUsersFromList', response.data.ocs.data.users))
- .catch((error) => context.commit('API_FAILURE', error));
+ .catch((error) => context.commit('API_FAILURE', error))
},
-
getPasswordPolicyMinLength(context) {
- if(OC.getCapabilities().password_policy && OC.getCapabilities().password_policy.minLength) {
- context.commit('setPasswordPolicyMinLength', OC.getCapabilities().password_policy.minLength);
- return OC.getCapabilities().password_policy.minLength;
+ if (OC.getCapabilities().password_policy && OC.getCapabilities().password_policy.minLength) {
+ context.commit('setPasswordPolicyMinLength', OC.getCapabilities().password_policy.minLength)
+ return OC.getCapabilities().password_policy.minLength
}
- return false;
+ return false
},
/**
* Add group
- *
- * @param {Object} context
+ *
+ * @param {Object} context store context
* @param {string} gid Group id
* @returns {Promise}
*/
addGroup(context, gid) {
return api.requireAdmin().then((response) => {
- return api.post(OC.linkToOCS(`cloud/groups`, 2), {groupid: gid})
+ return api.post(OC.linkToOCS(`cloud/groups`, 2), { groupid: gid })
.then((response) => {
- context.commit('addGroup', {gid: gid, displayName: gid})
- return {gid: gid, displayName: gid}
+ context.commit('addGroup', { gid: gid, displayName: gid })
+ return { gid: gid, displayName: gid }
})
- .catch((error) => {throw error;});
+ .catch((error) => { throw error })
}).catch((error) => {
- context.commit('API_FAILURE', { gid, error });
+ context.commit('API_FAILURE', { gid, error })
// let's throw one more time to prevent the view
// from adding the user to a group that doesn't exists
- throw error;
- });
+ throw error
+ })
},
/**
* Remove group
- *
- * @param {Object} context
+ *
+ * @param {Object} context store context
* @param {string} gid Group id
* @returns {Promise}
*/
@@ -320,15 +320,15 @@ const actions = {
return api.requireAdmin().then((response) => {
return api.delete(OC.linkToOCS(`cloud/groups/${gid}`, 2))
.then((response) => context.commit('removeGroup', gid))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { gid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { gid, error }))
},
/**
* Add user to group
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.gid Group id
* @returns {Promise}
@@ -337,15 +337,15 @@ const actions = {
return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`cloud/users/${userid}/groups`, 2), { groupid: gid })
.then((response) => context.commit('addUserGroup', { userid, gid }))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
},
/**
* Remove user from group
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.gid Group id
* @returns {Promise}
@@ -354,37 +354,37 @@ const actions = {
return api.requireAdmin().then((response) => {
return api.delete(OC.linkToOCS(`cloud/users/${userid}/groups`, 2), { groupid: gid })
.then((response) => context.commit('removeUserGroup', { userid, gid }))
- .catch((error) => {throw error;});
+ .catch((error) => { throw error })
}).catch((error) => {
- context.commit('API_FAILURE', { userid, error });
+ context.commit('API_FAILURE', { userid, error })
// let's throw one more time to prevent
// the view from removing the user row on failure
- throw error;
- });
+ throw error
+ })
},
/**
* Add user to group admin
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.gid Group id
* @returns {Promise}
*/
addUserSubAdmin(context, { userid, gid }) {
return api.requireAdmin().then((response) => {
- return api.post(OC.linkToOCS(`cloud/users/${userid}/subadmins`, 2), { groupid: gid })
+ return api.post(OC.linkToOCS(`cloud/users/${userid}/subadmins`, 2), { groupid: gid })
.then((response) => context.commit('addUserSubAdmin', { userid, gid }))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
},
/**
* Remove user from group admin
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.gid Group id
* @returns {Promise}
@@ -393,44 +393,44 @@ const actions = {
return api.requireAdmin().then((response) => {
return api.delete(OC.linkToOCS(`cloud/users/${userid}/subadmins`, 2), { groupid: gid })
.then((response) => context.commit('removeUserSubAdmin', { userid, gid }))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
},
/**
* Mark all user devices for remote wipe
*
- * @param {Object} context
+ * @param {Object} context store context
* @param {string} userid User id
* @returns {Promise}
*/
wipeUserDevices(context, userid) {
return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`cloud/users/${userid}/wipe`, 2))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
},
/**
* Delete a user
- *
- * @param {Object} context
- * @param {string} userid User id
+ *
+ * @param {Object} context store context
+ * @param {string} userid User id
* @returns {Promise}
*/
deleteUser(context, userid) {
return api.requireAdmin().then((response) => {
return api.delete(OC.linkToOCS(`cloud/users/${userid}`, 2))
.then((response) => context.commit('deleteUser', userid))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
},
/**
* Add a user
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.password User password
* @param {string} options.displayName User display name
@@ -440,93 +440,93 @@ const actions = {
* @param {string} options.quota User email
* @returns {Promise}
*/
- addUser({commit, dispatch}, { userid, password, displayName, email, groups, subadmin, quota, language }) {
+ addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language }) {
return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`cloud/users`, 2), { userid, password, displayName, email, groups, subadmin, quota, language })
.then((response) => dispatch('addUserData', userid || response.data.ocs.data.id))
- .catch((error) => {throw error;});
+ .catch((error) => { throw error })
}).catch((error) => {
- commit('API_FAILURE', { userid, error });
- throw error;
- });
+ commit('API_FAILURE', { userid, error })
+ throw error
+ })
},
/**
* Get user data and commit addition
- *
- * @param {Object} context
- * @param {string} userid User id
+ *
+ * @param {Object} context store context
+ * @param {string} userid User id
* @returns {Promise}
*/
addUserData(context, userid) {
return api.requireAdmin().then((response) => {
return api.get(OC.linkToOCS(`cloud/users/${userid}`, 2))
.then((response) => context.commit('addUserData', response))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
},
- /** Enable or disable user
- *
- * @param {Object} context
- * @param {Object} options
+ /** Enable or disable user
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {string} options.userid User id
* @param {boolean} options.enabled User enablement status
* @returns {Promise}
*/
enableDisableUser(context, { userid, enabled = true }) {
- let userStatus = enabled ? 'enable' : 'disable';
+ let userStatus = enabled ? 'enable' : 'disable'
return api.requireAdmin().then((response) => {
return api.put(OC.linkToOCS(`cloud/users/${userid}/${userStatus}`, 2))
.then((response) => context.commit('enableDisableUser', { userid, enabled }))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
},
/**
* Edit user data
- *
- * @param {Object} context
- * @param {Object} options
+ *
+ * @param {Object} context store context
+ * @param {Object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.key User field to edit
* @param {string} options.value Value of the change
* @returns {Promise}
*/
setUserData(context, { userid, key, value }) {
- let allowedEmpty = ['email', 'displayname'];
+ let allowedEmpty = ['email', 'displayname']
if (['email', 'language', 'quota', 'displayname', 'password'].indexOf(key) !== -1) {
// We allow empty email or displayname
- if (typeof value === 'string' &&
- (
- (allowedEmpty.indexOf(key) === -1 && value.length > 0) ||
- allowedEmpty.indexOf(key) !== -1
+ if (typeof value === 'string'
+ && (
+ (allowedEmpty.indexOf(key) === -1 && value.length > 0)
+ || allowedEmpty.indexOf(key) !== -1
)
) {
return api.requireAdmin().then((response) => {
return api.put(OC.linkToOCS(`cloud/users/${userid}`, 2), { key: key, value: value })
.then((response) => context.commit('setUserData', { userid, key, value }))
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
}
}
- return Promise.reject(new Error('Invalid request data'));
+ return Promise.reject(new Error('Invalid request data'))
},
/**
* Send welcome mail
- *
- * @param {Object} context
- * @param {string} userid User id
+ *
+ * @param {Object} context store context
+ * @param {string} userid User id
* @returns {Promise}
*/
sendWelcomeMail(context, userid) {
return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`cloud/users/${userid}/welcome`, 2))
.then(response => true)
- .catch((error) => {throw error;});
- }).catch((error) => context.commit('API_FAILURE', { userid, error }));
+ .catch((error) => { throw error })
+ }).catch((error) => context.commit('API_FAILURE', { userid, error }))
}
-};
+}
-export default { state, mutations, getters, actions };
+export default { state, mutations, getters, actions }
diff --git a/apps/settings/src/views/Apps.vue b/apps/settings/src/views/Apps.vue
index 35caf7cea59..2e0649f7f56 100644
--- a/apps/settings/src/views/Apps.vue
+++ b/apps/settings/src/views/Apps.vue
@@ -21,8 +21,10 @@
-->
<template>
- <Content app-name="settings" :class="{ 'with-app-sidebar': currentApp}"
- :content-class="{ 'icon-loading': loadingList }" :navigation-class="{ 'icon-loading': loading }">
+ <Content app-name="settings"
+ :class="{ 'with-app-sidebar': currentApp}"
+ :content-class="{ 'icon-loading': loadingList }"
+ :navigation-class="{ 'icon-loading': loading }">
<AppNavigation>
<ul id="appscategories">
<AppNavigationItem v-for="item in menu" :key="item.key" :item="item" />
@@ -38,32 +40,22 @@
</template>
<script>
-import {
+import {
AppContent,
AppNavigation,
AppNavigationItem,
AppSidebar,
Content
-} from 'nextcloud-vue';
-import AppList from '../components/appList';
-import Vue from 'vue';
+} from 'nextcloud-vue'
+import AppList from '../components/AppList'
+import Vue from 'vue'
import VueLocalStorage from 'vue-localstorage'
-import AppDetails from '../components/appDetails';
+import AppDetails from '../components/AppDetails'
Vue.use(VueLocalStorage)
export default {
name: 'Apps',
- props: {
- category: {
- type: String,
- default: 'installed',
- },
- id: {
- type: String,
- default: '',
- }
- },
components: {
AppContent,
AppNavigation,
@@ -73,110 +65,88 @@ export default {
AppDetails,
AppList
},
- methods: {
- setSearch(query) {
- this.searchQuery = query;
- },
- resetSearch() {
- this.setSearch('');
+ props: {
+ category: {
+ type: String,
+ default: 'installed'
},
- hideAppDetails() {
- this.$router.push({
- name: 'apps-category',
- params: {category: this.category}
- })
+ id: {
+ type: String,
+ default: ''
}
},
- beforeMount() {
- this.$store.dispatch('getCategories');
- this.$store.dispatch('getAllApps');
- this.$store.dispatch('getGroups', {offset: 0, limit: 5});
- this.$store.commit('setUpdateCount', this.$store.getters.getServerData.updateCount)
- },
- mounted() {
- /**
- * Register search
- */
- this.appSearch = new OCA.Search(this.setSearch, this.resetSearch);
- },
data() {
return {
searchQuery: ''
}
},
- watch: {
- category: function (val, old) {
- this.setSearch('');
- }
- },
computed: {
loading() {
- return this.$store.getters.loading('categories');
+ return this.$store.getters.loading('categories')
},
loadingList() {
- return this.$store.getters.loading('list');
+ return this.$store.getters.loading('list')
},
currentApp() {
- return this.apps.find(app => app.id === this.id );
+ return this.apps.find(app => app.id === this.id)
},
categories() {
- return this.$store.getters.getCategories;
+ return this.$store.getters.getCategories
},
apps() {
- return this.$store.getters.getAllApps;
+ return this.$store.getters.getAllApps
},
updateCount() {
- return this.$store.getters.getUpdateCount;
+ return this.$store.getters.getUpdateCount
},
settings() {
- return this.$store.getters.getServerData;
+ return this.$store.getters.getServerData
},
// BUILD APP NAVIGATION MENU OBJECT
menu() {
// Data provided php side
- let categories = this.$store.getters.getCategories;
- categories = Array.isArray(categories) ? categories : [];
+ let categories = this.$store.getters.getCategories
+ categories = Array.isArray(categories) ? categories : []
// Map groups
categories = categories.map(category => {
- let item = {};
- item.id = 'app-category-' + category.ident;
- item.icon = 'icon-category-' + category.ident;
- item.classes = []; // empty classes, active will be set later
+ let item = {}
+ item.id = 'app-category-' + category.ident
+ item.icon = 'icon-category-' + category.ident
+ item.classes = [] // empty classes, active will be set later
item.router = { // router link to
name: 'apps-category',
- params: {category: category.ident}
- };
- item.text = category.displayName;
-
- return item;
- });
+ params: { category: category.ident }
+ }
+ item.text = category.displayName
+ return item
+ })
// Add everyone group
let defaultCategories = [
{
id: 'app-category-your-apps',
classes: [],
- router: {name: 'apps'},
+ router: { name: 'apps' },
icon: 'icon-category-installed',
- text: t('settings', 'Your apps'),
+ text: t('settings', 'Your apps')
},
{
id: 'app-category-enabled',
classes: [],
icon: 'icon-category-enabled',
- router: {name: 'apps-category', params: {category: 'enabled'}},
- text: t('settings', 'Active apps'),
+ router: { name: 'apps-category', params: { category: 'enabled' } },
+ text: t('settings', 'Active apps')
}, {
id: 'app-category-disabled',
classes: [],
icon: 'icon-category-disabled',
- router: {name: 'apps-category', params: {category: 'disabled'}},
- text: t('settings', 'Disabled apps'),
+ router: { name: 'apps-category', params: { category: 'disabled' } },
+ text: t('settings', 'Disabled apps')
}
- ];
+ ]
if (!this.settings.appstoreEnabled) {
return defaultCategories
@@ -187,40 +157,71 @@ export default {
id: 'app-category-updates',
classes: [],
icon: 'icon-download',
- router: {name: 'apps-category', params: {category: 'updates'}},
+ router: { name: 'apps-category', params: { category: 'updates' } },
text: t('settings', 'Updates'),
- utils: {counter: this.$store.getters.getUpdateCount}
- });
+ utils: { counter: this.$store.getters.getUpdateCount }
+ })
}
defaultCategories.push({
id: 'app-category-app-bundles',
classes: [],
icon: 'icon-category-app-bundles',
- router: {name: 'apps-category', params: {category: 'app-bundles'}},
- text: t('settings', 'App bundles'),
- });
+ router: { name: 'apps-category', params: { category: 'app-bundles' } },
+ text: t('settings', 'App bundles')
+ })
- categories = defaultCategories.concat(categories);
+ categories = defaultCategories.concat(categories)
// Set current group as active
- let activeGroup = categories.findIndex(group => group.id === 'app-category-' + this.category);
+ let activeGroup = categories.findIndex(group => group.id === 'app-category-' + this.category)
if (activeGroup >= 0) {
- categories[activeGroup].classes.push('active');
+ categories[activeGroup].classes.push('active')
} else {
- categories[0].classes.push('active');
+ categories[0].classes.push('active')
}
categories.push({
id: 'app-developer-docs',
classes: [],
href: this.settings.developerDocumentation,
- text: t('settings', 'Developer documentation') + ' ↗',
- });
+ text: t('settings', 'Developer documentation') + ' ↗'
+ })
// Return
return categories
+ }
+ },
+ watch: {
+ category: function(val, old) {
+ this.setSearch('')
+ }
+ },
+ beforeMount() {
+ this.$store.dispatch('getCategories')
+ this.$store.dispatch('getAllApps')
+ this.$store.dispatch('getGroups', { offset: 0, limit: 5 })
+ this.$store.commit('setUpdateCount', this.$store.getters.getServerData.updateCount)
+ },
+ mounted() {
+ /**
+ * Register search
+ */
+ this.appSearch = new OCA.Search(this.setSearch, this.resetSearch)
+ },
+ methods: {
+ setSearch(query) {
+ this.searchQuery = query
},
+ resetSearch() {
+ this.setSearch('')
+ },
+ hideAppDetails() {
+ this.$router.push({
+ name: 'apps-category',
+ params: { category: this.category }
+ })
+ }
}
}
</script>
diff --git a/apps/settings/src/views/Users.vue b/apps/settings/src/views/Users.vue
index dcbef1cd799..3c3082fc077 100644
--- a/apps/settings/src/views/Users.vue
+++ b/apps/settings/src/views/Users.vue
@@ -23,47 +23,69 @@
<template>
<Content app-name="settings" :navigation-class="{ 'icon-loading': loadingAddGroup }">
<AppNavigation>
- <AppNavigationNew button-id="new-user-button" :text="t('settings','New user')" button-class="icon-add" @click="toggleNewUserMenu" />
+ <AppNavigationNew button-id="new-user-button"
+ :text="t('settings','New user')"
+ button-class="icon-add"
+ @click="toggleNewUserMenu" />
<ul id="usergrouplist">
<AppNavigationItem v-for="item in menu" :key="item.key" :item="item" />
</ul>
<AppNavigationSettings>
<div>
- <p>{{t('settings', 'Default quota:')}}</p>
- <Multiselect :value="defaultQuota" :options="quotaOptions"
- tag-placeholder="create" :placeholder="t('settings', 'Select default quota')"
- label="label" track-by="id"
- :allowEmpty="false" :taggable="true"
- @tag="validateQuota" @input="setDefaultQuota">
- </Multiselect>
-
+ <p>{{ t('settings', 'Default quota:') }}</p>
+ <Multiselect :value="defaultQuota"
+ :options="quotaOptions"
+ tag-placeholder="create"
+ :placeholder="t('settings', 'Select default quota')"
+ label="label"
+ track-by="id"
+ :allow-empty="false"
+ :taggable="true"
+ @tag="validateQuota"
+ @input="setDefaultQuota" />
</div>
<div>
- <input type="checkbox" id="showLanguages" class="checkbox" v-model="showLanguages">
- <label for="showLanguages">{{t('settings', 'Show Languages')}}</label>
+ <input id="showLanguages"
+ v-model="showLanguages"
+ type="checkbox"
+ class="checkbox">
+ <label for="showLanguages">{{ t('settings', 'Show Languages') }}</label>
</div>
<div>
- <input type="checkbox" id="showLastLogin" class="checkbox" v-model="showLastLogin">
- <label for="showLastLogin">{{t('settings', 'Show last login')}}</label>
+ <input id="showLastLogin"
+ v-model="showLastLogin"
+ type="checkbox"
+ class="checkbox">
+ <label for="showLastLogin">{{ t('settings', 'Show last login') }}</label>
</div>
<div>
- <input type="checkbox" id="showUserBackend" class="checkbox" v-model="showUserBackend">
- <label for="showUserBackend">{{t('settings', 'Show user backend')}}</label>
+ <input id="showUserBackend"
+ v-model="showUserBackend"
+ type="checkbox"
+ class="checkbox">
+ <label for="showUserBackend">{{ t('settings', 'Show user backend') }}</label>
</div>
<div>
- <input type="checkbox" id="showStoragePath" class="checkbox" v-model="showStoragePath">
- <label for="showStoragePath">{{t('settings', 'Show storage path')}}</label>
+ <input id="showStoragePath"
+ v-model="showStoragePath"
+ type="checkbox"
+ class="checkbox">
+ <label for="showStoragePath">{{ t('settings', 'Show storage path') }}</label>
</div>
</AppNavigationSettings>
</AppNavigation>
<AppContent>
- <UserList #content :users="users" :showConfig="showConfig" :selectedGroup="selectedGroup" :externalActions="externalActions" />
+ <UserList #content
+ :users="users"
+ :show-config="showConfig"
+ :selected-group="selectedGroup"
+ :external-actions="externalActions" />
</AppContent>
</Content>
</template>
<script>
-import Vue from 'vue';
+import Vue from 'vue'
import VueLocalStorage from 'vue-localstorage'
import {
AppContent,
@@ -71,52 +93,35 @@ import {
AppNavigationItem,
AppNavigationNew,
AppNavigationSettings,
- AppSidebar,
Content,
Multiselect
-} from 'nextcloud-vue';
-import UserList from '../components/userList';
-import api from '../store/api';
+} from 'nextcloud-vue'
+import UserList from '../components/UserList'
Vue.use(VueLocalStorage)
export default {
name: 'Users',
- props: ['selectedGroup'],
components: {
AppContent,
AppNavigation,
AppNavigationItem,
AppNavigationNew,
AppNavigationSettings,
- AppSidebar,
Content,
UserList,
- Multiselect,
+ Multiselect
},
- beforeMount() {
- this.$store.commit('initGroups', {
- groups: this.$store.getters.getServerData.groups,
- orderBy: this.$store.getters.getServerData.sortGroups,
- userCount: this.$store.getters.getServerData.userCount
- });
- this.$store.dispatch('getPasswordPolicyMinLength');
- },
- created() {
- // init the OCA.Settings.UserList object
- // and add the registerAction method
- Object.assign(OCA, {
- Settings: {
- UserList: {
- registerAction: this.registerAction
- }
- }
- });
+ props: {
+ selectedGroup: {
+ type: String,
+ default: null
+ }
},
data() {
return {
// default quota is set to unlimited
- unlimitedQuota: {id: 'none', label: t('settings', 'Unlimited')},
+ unlimitedQuota: { id: 'none', label: t('settings', 'Unlimited') },
// temporary value used for multiselect change
selectedQuota: false,
externalActions: [],
@@ -131,217 +136,103 @@ export default {
}
}
},
- methods: {
- toggleNewUserMenu() {
- this.showConfig.showNewUserForm = !this.showConfig.showNewUserForm;
- if (this.showConfig.showNewUserForm) {
- Vue.nextTick(() => {
- window.newusername.focus();
- });
- }
- },
- getLocalstorage(key) {
- // force initialization
- let localConfig = this.$localStorage.get(key);
- // if localstorage is null, fallback to original values
- this.showConfig[key] = localConfig !== null ? localConfig === 'true' : this.showConfig[key];
- return this.showConfig[key];
- },
- setLocalStorage(key, status) {
- this.showConfig[key] = status;
- this.$localStorage.set(key, status);
- return status;
- },
- removeGroup(groupid) {
- let self = this;
- // TODO migrate to a vue js confirm dialog component
- OC.dialogs.confirm(
- t('settings', 'You are about to remove the group {group}. The users will NOT be deleted.', {group: groupid}),
- t('settings','Please confirm the group removal '),
- function (success) {
- if (success) {
- self.$store.dispatch('removeGroup', groupid);
- }
- }
- );
- },
-
- /**
- * Dispatch default quota set request
- *
- * @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
- * @returns {string}
- */
- setDefaultQuota(quota = 'none') {
- this.$store.dispatch('setAppConfig', {
- app: 'files',
- key: 'default_quota',
- // ensure we only send the preset id
- value: quota.id ? quota.id : quota
- }).then(() => {
- if (typeof quota !== 'object') {
- quota = {id: quota, label: quota};
- }
- this.defaultQuota = 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) {
- return this.setDefaultQuota('none');
- } else {
- // unify format output
- return this.setDefaultQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)));
- }
- // if no valid do not change
- return false;
- },
-
- /**
- * Register a new action for the user menu
- *
- * @param {string} icon the icon class
- * @param {string} text the text to display
- * @param {function} action the function to run
- */
- registerAction(icon, text, action) {
- this.externalActions.push({
- icon: icon,
- text: text,
- action: action
- });
- return this.externalActions;
- },
-
- /**
- * Create a new group
- *
- * @param {Object} event The form submit event
- */
- createGroup(event) {
- let gid = event.target[0].value;
- this.loadingAddGroup = true;
- this.$store.dispatch('addGroup', gid)
- .then(() => {
- this.showAddGroupEntry = false;
- this.loadingAddGroup = false;
- this.$router.push({
- name: 'group',
- params: {
- selectedGroup: gid
- }
- })
- })
- .catch(() => {
- this.loadingAddGroup = false;
- });
- }
- },
computed: {
users() {
- return this.$store.getters.getUsers;
+ return this.$store.getters.getUsers
},
usersOffset() {
- return this.$store.getters.getUsersOffset;
+ return this.$store.getters.getUsersOffset
},
usersLimit() {
- return this.$store.getters.getUsersLimit;
+ return this.$store.getters.getUsersLimit
},
// Local settings
showLanguages: {
- get: function() {return this.getLocalstorage('showLanguages')},
+ get: function() { return this.getLocalstorage('showLanguages') },
set: function(status) {
- this.setLocalStorage('showLanguages', status);
+ this.setLocalStorage('showLanguages', status)
}
},
showLastLogin: {
- get: function() {return this.getLocalstorage('showLastLogin')},
+ get: function() { return this.getLocalstorage('showLastLogin') },
set: function(status) {
- this.setLocalStorage('showLastLogin', status);
+ this.setLocalStorage('showLastLogin', status)
}
},
showUserBackend: {
- get: function() {return this.getLocalstorage('showUserBackend')},
+ get: function() { return this.getLocalstorage('showUserBackend') },
set: function(status) {
- this.setLocalStorage('showUserBackend', status);
+ this.setLocalStorage('showUserBackend', status)
}
},
showStoragePath: {
- get: function() {return this.getLocalstorage('showStoragePath')},
+ get: function() { return this.getLocalstorage('showStoragePath') },
set: function(status) {
- this.setLocalStorage('showStoragePath', status);
+ this.setLocalStorage('showStoragePath', status)
}
},
userCount() {
- return this.$store.getters.getUserCount;
+ return this.$store.getters.getUserCount
},
settings() {
- return this.$store.getters.getServerData;
+ return this.$store.getters.getServerData
},
// default quota
quotaOptions() {
// convert the preset array into objects
- let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({id:cur, label:cur}), []);
+ let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({ id: cur, label: cur }), [])
// add default presets
- quotaPreset.unshift(this.unlimitedQuota);
- return quotaPreset;
+ quotaPreset.unshift(this.unlimitedQuota)
+ return quotaPreset
},
// mapping saved values to objects
defaultQuota: {
get: function() {
if (this.selectedQuota !== false) {
- return this.selectedQuota;
+ return this.selectedQuota
}
if (this.settings.defaultQuota !== this.unlimitedQuota.id && OC.Util.computerFileSize(this.settings.defaultQuota) >= 0) {
// if value is valid, let's map the quotaOptions or return custom quota
- return {id:this.settings.defaultQuota, label:this.settings.defaultQuota};
+ return { id: this.settings.defaultQuota, label: this.settings.defaultQuota }
}
- return this.unlimitedQuota; // unlimited
+ return this.unlimitedQuota // unlimited
},
set: function(quota) {
- this.selectedQuota = quota;
+ this.selectedQuota = quota
}
-
+
},
// BUILD APP NAVIGATION MENU OBJECT
menu() {
// Data provided php side
- let self = this;
- let groups = this.$store.getters.getGroups;
- groups = Array.isArray(groups) ? groups : [];
+ let self = this
+ let groups = this.$store.getters.getGroups
+ groups = Array.isArray(groups) ? groups : []
// Map groups
groups = groups.map(group => {
- let item = {};
- item.id = group.id.replace(' ', '_');
- item.key = item.id;
+ let item = {}
+ item.id = group.id.replace(' ', '_')
+ item.key = item.id
item.utils = {}
// router link to
item.router = {
name: 'group',
- params: {selectedGroup: group.id}
- };
+ params: { selectedGroup: group.id }
+ }
// group name
- item.text = group.name;
- item.title = group.name;
+ item.text = group.name
+ item.title = group.name
// users count for all groups
if (group.usercount - group.disabled > 0 || group.usercount === -1) {
- item.utils.counter = group.usercount - group.disabled;
+ item.utils.counter = group.usercount - group.disabled
}
if (item.id !== 'admin' && item.id !== 'disabled' && this.settings.isAdmin) {
@@ -352,65 +243,64 @@ export default {
action: function() {
self.removeGroup(group.id)
}
- }];
- };
- return item;
- });
+ }]
+ }
+ return item
+ })
// Every item is added on top of the array, so we're going backward
// Groups, separator, disabled, admin, everyone
// Add separator
- let realGroups = groups.find((group) => {return group.id !== 'disabled' && group.id !== 'admin'});
- realGroups = typeof realGroups === 'undefined' ? [] : realGroups;
- realGroups = Array.isArray(realGroups) ? realGroups : [realGroups];
+ let realGroups = groups.find((group) => { return group.id !== 'disabled' && group.id !== 'admin' })
+ realGroups = typeof realGroups === 'undefined' ? [] : realGroups
+ realGroups = Array.isArray(realGroups) ? realGroups : [realGroups]
if (realGroups.length > 0) {
let separator = {
caption: true,
text: t('settings', 'Groups')
- };
- groups.unshift(separator);
+ }
+ groups.unshift(separator)
}
// Adjust admin and disabled groups
- let adminGroup = groups.find(group => group.id == 'admin');
- let disabledGroup = groups.find(group => group.id == 'disabled');
+ let adminGroup = groups.find(group => group.id === 'admin')
+ let disabledGroup = groups.find(group => group.id === 'disabled')
// filter out admin and disabled
- groups = groups.filter(group => ['admin', 'disabled'].indexOf(group.id) === -1);
+ groups = groups.filter(group => ['admin', 'disabled'].indexOf(group.id) === -1)
if (adminGroup && adminGroup.text) {
- adminGroup.text = t('settings', 'Admins'); // rename admin group
- adminGroup.icon = 'icon-user-admin'; // set icon
- groups.unshift(adminGroup); // add admin group if present
+ adminGroup.text = t('settings', 'Admins') // rename admin group
+ adminGroup.icon = 'icon-user-admin' // set icon
+ groups.unshift(adminGroup) // add admin group if present
}
if (disabledGroup && disabledGroup.text) {
- disabledGroup.text = t('settings', 'Disabled users'); // rename disabled group
- disabledGroup.icon = 'icon-disabled-users'; // set icon
+ disabledGroup.text = t('settings', 'Disabled users') // rename disabled group
+ disabledGroup.icon = 'icon-disabled-users' // set icon
if (disabledGroup.utils && (
- disabledGroup.utils.counter > 0 // add disabled if not empty
- || disabledGroup.utils.counter === -1) // add disabled if ldap enabled
+ disabledGroup.utils.counter > 0 // add disabled if not empty
+ || disabledGroup.utils.counter === -1) // add disabled if ldap enabled
) {
- groups.unshift(disabledGroup);
+ groups.unshift(disabledGroup)
}
}
-
// Add everyone group
let everyoneGroup = {
id: 'everyone',
key: 'everyone',
icon: 'icon-contacts-dark',
- router: {name:'users'},
- text: t('settings', 'Everyone'),
- };
+ router: { name: 'users' },
+ text: t('settings', 'Everyone')
+ }
// users count
if (this.userCount > 0) {
Vue.set(everyoneGroup, 'utils', {
counter: this.userCount
- });
+ })
}
- groups.unshift(everyoneGroup);
+ groups.unshift(everyoneGroup)
let addGroup = {
id: 'addgroup',
@@ -418,7 +308,7 @@ export default {
icon: 'icon-add',
text: t('settings', 'Add group'),
classes: this.loadingAddGroup ? 'icon-loading-small' : ''
- };
+ }
if (this.showAddGroupEntry) {
Vue.set(addGroup, 'edit', {
text: t('settings', 'Add group'),
@@ -426,8 +316,8 @@ export default {
reset: function() {
self.showAddGroupEntry = false
}
- });
- addGroup.classes = 'editing';
+ })
+ addGroup.classes = 'editing'
} else {
Vue.set(addGroup, 'action', function() {
self.showAddGroupEntry = true
@@ -437,10 +327,141 @@ export default {
})
})
}
- groups.unshift(addGroup);
+ groups.unshift(addGroup)
- return groups;
+ return groups
+ }
+ },
+ beforeMount() {
+ this.$store.commit('initGroups', {
+ groups: this.$store.getters.getServerData.groups,
+ orderBy: this.$store.getters.getServerData.sortGroups,
+ userCount: this.$store.getters.getServerData.userCount
+ })
+ this.$store.dispatch('getPasswordPolicyMinLength')
+ },
+ created() {
+ // init the OCA.Settings.UserList object
+ // and add the registerAction method
+ Object.assign(OCA, {
+ Settings: {
+ UserList: {
+ registerAction: this.registerAction
+ }
+ }
+ })
+ },
+ methods: {
+ toggleNewUserMenu() {
+ this.showConfig.showNewUserForm = !this.showConfig.showNewUserForm
+ if (this.showConfig.showNewUserForm) {
+ Vue.nextTick(() => {
+ window.newusername.focus()
+ })
+ }
},
+ getLocalstorage(key) {
+ // force initialization
+ let localConfig = this.$localStorage.get(key)
+ // if localstorage is null, fallback to original values
+ this.showConfig[key] = localConfig !== null ? localConfig === 'true' : this.showConfig[key]
+ return this.showConfig[key]
+ },
+ setLocalStorage(key, status) {
+ this.showConfig[key] = status
+ this.$localStorage.set(key, status)
+ return status
+ },
+ removeGroup(groupid) {
+ let self = this
+ // TODO migrate to a vue js confirm dialog component
+ OC.dialogs.confirm(
+ t('settings', 'You are about to remove the group {group}. The users will NOT be deleted.', { group: groupid }),
+ t('settings', 'Please confirm the group removal '),
+ function(success) {
+ if (success) {
+ self.$store.dispatch('removeGroup', groupid)
+ }
+ }
+ )
+ },
+
+ /**
+ * Dispatch default quota set request
+ *
+ * @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
+ */
+ setDefaultQuota(quota = 'none') {
+ this.$store.dispatch('setAppConfig', {
+ app: 'files',
+ key: 'default_quota',
+ // ensure we only send the preset id
+ value: quota.id ? quota.id : quota
+ }).then(() => {
+ if (typeof quota !== 'object') {
+ quota = { id: quota, label: quota }
+ }
+ this.defaultQuota = 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) {
+ return this.setDefaultQuota('none')
+ } else {
+ // unify format output
+ return this.setDefaultQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)))
+ }
+ },
+
+ /**
+ * Register a new action for the user menu
+ *
+ * @param {string} icon the icon class
+ * @param {string} text the text to display
+ * @param {Function} action the function to run
+ * @returns {Array}
+ */
+ registerAction(icon, text, action) {
+ this.externalActions.push({
+ icon: icon,
+ text: text,
+ action: action
+ })
+ return this.externalActions
+ },
+
+ /**
+ * Create a new group
+ *
+ * @param {Object} event The form submit event
+ */
+ createGroup(event) {
+ let gid = event.target[0].value
+ this.loadingAddGroup = true
+ this.$store.dispatch('addGroup', gid)
+ .then(() => {
+ this.showAddGroupEntry = false
+ this.loadingAddGroup = false
+ this.$router.push({
+ name: 'group',
+ params: {
+ selectedGroup: gid
+ }
+ })
+ })
+ .catch(() => {
+ this.loadingAddGroup = false
+ })
+ }
}
}
</script>