aboutsummaryrefslogtreecommitdiffstats
path: root/settings/src
diff options
context:
space:
mode:
Diffstat (limited to 'settings/src')
-rw-r--r--settings/src/.jshintrc3
-rw-r--r--settings/src/App.vue16
-rw-r--r--settings/src/components/appNavigation.vue32
-rw-r--r--settings/src/components/appNavigation/navigationItem.vue108
-rw-r--r--settings/src/components/popoverMenu.vue18
-rw-r--r--settings/src/components/popoverMenu/popoverItem.vue23
-rw-r--r--settings/src/components/userList.vue205
-rw-r--r--settings/src/components/userList/userRow.vue370
-rw-r--r--settings/src/main.js20
-rw-r--r--settings/src/router.js23
-rw-r--r--settings/src/store/api.js50
-rw-r--r--settings/src/store/index.js24
-rw-r--r--settings/src/store/settings.js18
-rw-r--r--settings/src/store/users.js380
-rw-r--r--settings/src/views/Users.vue152
15 files changed, 1442 insertions, 0 deletions
diff --git a/settings/src/.jshintrc b/settings/src/.jshintrc
new file mode 100644
index 00000000000..e688987c23f
--- /dev/null
+++ b/settings/src/.jshintrc
@@ -0,0 +1,3 @@
+{
+ "esversion": 6
+}
diff --git a/settings/src/App.vue b/settings/src/App.vue
new file mode 100644
index 00000000000..3b1cd53ca35
--- /dev/null
+++ b/settings/src/App.vue
@@ -0,0 +1,16 @@
+<template>
+ <router-view></router-view>
+</template>
+
+<script>
+export default {
+ name: 'App',
+ beforeMount: function () {
+ // importing server data into the store
+ const serverDataElmt = document.getElementById('serverData');
+ if (serverDataElmt !== null) {
+ this.$store.commit('setServerData', JSON.parse(document.getElementById('serverData').dataset.server));
+ }
+ }
+}
+</script>
diff --git a/settings/src/components/appNavigation.vue b/settings/src/components/appNavigation.vue
new file mode 100644
index 00000000000..256f71d18cc
--- /dev/null
+++ b/settings/src/components/appNavigation.vue
@@ -0,0 +1,32 @@
+<template>
+ <div id="app-navigation">
+ <div class="app-navigation-new" v-if="menu.new">
+ <button type="button" :id="menu.new.id" :class="menu.new.icon" @click="menu.new.action">{{menu.new.text}}</button>
+ </div>
+ <ul :id="menu.id">
+ <navigation-item v-for="(item, key) in menu.items" :item="item" :key="key" />
+ </ul>
+ <div id="app-settings">
+ <div id="app-settings-header">
+ <button class="settings-button"
+ data-apps-slide-toggle="#app-settings-content"
+ >{{t('settings', 'Settings')}}</button>
+ </div>
+ <div id="app-settings-content">
+ <slot name="settings-content"></slot>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import navigationItem from './appNavigation/navigationItem';
+
+export default {
+ name: 'appNavigation',
+ props: ['menu'],
+ components: {
+ navigationItem
+ }
+}
+</script>
diff --git a/settings/src/components/appNavigation/navigationItem.vue b/settings/src/components/appNavigation/navigationItem.vue
new file mode 100644
index 00000000000..19b0d90e3f4
--- /dev/null
+++ b/settings/src/components/appNavigation/navigationItem.vue
@@ -0,0 +1,108 @@
+<template>
+ <li :id="item.id" :class="[{'icon-loading-small': item.loading, 'open': item.opened, 'collapsible': item.collapsible&&item.children&&item.children.length>0 }, item.classes]">
+
+ <!-- Bullet -->
+ <div v-if="item.bullet" class="app-navigation-entry-bullet" :style="{ backgroundColor: item.bullet }"></div>
+
+ <!-- Main link -->
+ <a :href="(item.href) ? item.href : '#' " @click="toggleCollapse" :class="item.icon" >{{item.text}}</a>
+
+ <!-- Popover, counter and button(s) -->
+ <div v-if="item.utils" class="app-navigation-entry-utils">
+ <ul>
+ <!-- counter -->
+ <li v-if="Number.isInteger(item.utils.counter)"
+ class="app-navigation-entry-utils-counter">{{item.utils.counter}}</li>
+
+ <!-- first action if only one action and counter -->
+ <li v-if="item.utils.actions && item.utils.actions.length === 1 && Number.isInteger(item.utils.counter)"
+ class="app-navigation-entry-utils-menu-button">
+ <button :class="item.utils.actions[0].icon"></button>
+ </li>
+
+ <!-- second action only two actions and no counter -->
+ <li v-else-if="item.utils.actions && item.utils.actions.length === 2 && !Number.isInteger(item.utils.counter)"
+ v-for="action in item.utils.actions" :key="action.action"
+ class="app-navigation-entry-utils-menu-button">
+ <button :class="action.icon"></button>
+ </li>
+
+ <!-- menu if only at least one action and counter OR two actions and no counter-->
+ <li v-else-if="item.utils.actions && item.utils.actions.length > 1 && (Number.isInteger(item.utils.counter) || item.utils.actions.length > 2)"
+ class="app-navigation-entry-utils-menu-button">
+ <button v-click-outside="hideMenu" @click="showMenu" ></button>
+ </li>
+ </ul>
+ </div>
+
+ <!-- if more than 2 actions or more than 1 actions with counter -->
+ <div v-if="item.utils && item.utils.actions && item.utils.actions.length > 1 && (Number.isInteger(item.utils.counter) || item.utils.actions.length > 2)"
+ class="app-navigation-entry-menu" :class="{ 'open': openedMenu }">
+ <popover-menu :menu="item.utils.actions"/>
+ </div>
+
+ <!-- undo entry -->
+ <div class="app-navigation-entry-deleted" v-if="item.undo">
+ <div class="app-navigation-entry-deleted-description">{{item.undo.text}}</div>
+ <button class="app-navigation-entry-deleted-button icon-history" :title="t('settings', 'Undo')"></button>
+ </div>
+
+ <!-- edit entry -->
+ <div class="app-navigation-entry-edit" v-if="item.edit">
+ <form>
+ <input type="text" v-model="item.text">
+ <input type="submit" value="" class="icon-confirm">
+ <input type="submit" value="" class="icon-close" @click.stop.prevent="cancelEdit">
+ </form>
+ </div>
+
+ <!-- if the item has children, inject the component with proper data -->
+ <ul v-if="item.children">
+ <navigation-item v-for="(item, key) in item.children" :item="item" :key="key" />
+ </ul>
+ </li>
+</template>
+
+<script>
+import popoverMenu from '../popoverMenu';
+import ClickOutside from 'vue-click-outside';
+import Vue from 'vue';
+
+export default {
+ name: 'navigationItem',
+ props: ['item'],
+ components: {
+ popoverMenu
+ },
+ directives: {
+ ClickOutside
+ },
+ data () {
+ return {
+ openedMenu: false
+ }
+ },
+ methods: {
+ showMenu () {
+ this.openedMenu = true;
+ },
+ hideMenu () {
+ this.openedMenu = false;
+ },
+ toggleCollapse () {
+ // if item.opened isn't set, Vue won't trigger view updates https://vuejs.org/v2/api/#Vue-set
+ // ternary is here to detect the undefined state of item.opened
+ Vue.set(this.item, 'opened', this.item.opened ? !this.item.opened : true);
+ },
+ cancelEdit () {
+ // remove the editing class
+ if (Array.isArray(this.item.classes))
+ this.item.classes = this.item.classes.filter(item => item !== 'editing');
+ }
+ },
+ mounted () {
+ // prevent click outside event with popupItem.
+ this.popupItem = this.$el;
+ },
+}
+</script>
diff --git a/settings/src/components/popoverMenu.vue b/settings/src/components/popoverMenu.vue
new file mode 100644
index 00000000000..92f62c5090d
--- /dev/null
+++ b/settings/src/components/popoverMenu.vue
@@ -0,0 +1,18 @@
+<template>
+ <ul>
+ <popover-item v-for="(item, key) in menu" :item="item" :key="key" />
+ </ul>
+</template>
+
+
+<script>
+import popoverItem from './popoverMenu/popoverItem';
+
+export default {
+ name: 'popoverMenu',
+ props: ['menu'],
+ components: {
+ popoverItem
+ }
+}
+</script>
diff --git a/settings/src/components/popoverMenu/popoverItem.vue b/settings/src/components/popoverMenu/popoverItem.vue
new file mode 100644
index 00000000000..84907341327
--- /dev/null
+++ b/settings/src/components/popoverMenu/popoverItem.vue
@@ -0,0 +1,23 @@
+<template>
+ <li>
+ <a @click="dispatchToStore" v-if="item.href" :href="(item.href) ? item.href : '#' ">
+ <span :class="item.icon"></span>
+ <span>{{item.text}}</span>
+ </a>
+ <button @click="dispatchToStore(item.action)" v-else>
+ <span :class="item.icon"></span>
+ <span>{{item.text}}</span>
+ </button>
+ </li>
+</template>
+
+<script>
+export default {
+ props: ['item'],
+ methods: {
+ dispatchToStore () {
+ this.$store.dispatch(this.item.action, this.item.data);
+ }
+ }
+}
+</script>
diff --git a/settings/src/components/userList.vue b/settings/src/components/userList.vue
new file mode 100644
index 00000000000..08153a44770
--- /dev/null
+++ b/settings/src/components/userList.vue
@@ -0,0 +1,205 @@
+<template>
+ <div id="app-content" class="user-list-grid" v-on:scroll.passive="onScroll">
+ <div class="row" id="grid-header" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
+ <div id="headerAvatar" class="avatar"></div>
+ <div id="headerName" class="name">{{ t('settings', 'Username') }}</div>
+ <div id="headerDisplayName" class="displayName">{{ t('settings', 'Full 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 id="headerSubAdmins" class="subadmins"
+ v-if="subAdminsGroups.length>0">{{ t('settings', 'Group admin for') }}</div>
+ <div id="headerQuota" class="quota">{{ t('settings', 'Quota') }}</div>
+ <div class="headerStorageLocation storageLocation"
+ v-if="showConfig.showStoragePath">{{ t('settings', 'Storage location') }}</div>
+ <div class="headerUserBackend userBackend"
+ v-if="showConfig.showUserBackend">{{ t('settings', 'User backend') }}</div>
+ <div class="headerLastLogin lastLogin"
+ v-if="showConfig.showLastLogin">{{ t('settings', 'Last login') }}</div>
+ <div class="userActions"></div>
+ </div>
+
+ <form class="row" id="new-user" v-show="showConfig.showNewUserForm"
+ v-on:submit.prevent="createUser" :disabled="loading"
+ :class="{'sticky': scrolled && showConfig.showNewUserForm}">
+ <div :class="loading?'icon-loading-small':'icon-add'"></div>
+ <div class="name">
+ <input id="newusername" type="text" required v-model="newUser.id"
+ :placeholder="t('settings', 'User name')" name="username"
+ autocomplete="off" autocapitalize="none" autocorrect="off"
+ pattern="[a-zA-Z0-9 _\.@\-']+">
+ </div>
+ <div class="displayName">
+ <input id="newdisplayname" type="text" v-model="newUser.displayName"
+ :placeholder="t('settings', 'Display name')" name="displayname"
+ autocomplete="off" autocapitalize="none" autocorrect="off">
+ </div>
+ <div class="password">
+ <input id="newuserpassword" type="password" v-model="newUser.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" type="email" v-model="newUser.mailAddress"
+ :required="newUser.password===''"
+ :placeholder="t('settings', 'Mail address')" name="email"
+ autocomplete="off" autocapitalize="none" autocorrect="off">
+ </div>
+ <div class="groups">
+ <multiselect :options="groups" v-model="newUser.groups"
+ :placeholder="t('settings', 'Add user in group')"
+ label="name" track-by="id" class="multiselect-vue"
+ :multiple="true" :close-on-select="false">
+ <span slot="noResult">{{t('settings','No result')}}</span>
+ </multiselect>
+ </div>
+ <div class="subadmins" v-if="subAdminsGroups.length>0">
+ <multiselect :options="subAdminsGroups" v-model="newUser.subAdminsGroups"
+ :placeholder="t('settings', 'Set user as admin for')"
+ label="name" track-by="id" class="multiselect-vue"
+ :multiple="true" :close-on-select="false">
+ <span slot="noResult">{{t('settings','No result')}}</span>
+ </multiselect>
+ </div>
+ <div class="quota">
+ <multiselect :options="quotaOptions" v-model="newUser.quota"
+ :placeholder="t('settings', 'Select user quota')"
+ label="label" track-by="id" class="multiselect-vue"
+ :allowEmpty="false" :taggable="true"
+ @tag="validateQuota" >
+ </multiselect>
+ </div>
+ <div class="storageLocation" v-if="showConfig.showStoragePath"></div>
+ <div class="userBackend" v-if="showConfig.showUserBackend"></div>
+ <div class="lastLogin" v-if="showConfig.showLastLogin"></div>
+ <div class="userActions">
+ <input type="submit" id="newsubmit" class="button primary icon-checkmark-white has-tooltip"
+ value="" :title="t('settings', 'Add a new user')">
+ <input type="reset" id="newreset" class="button icon-close has-tooltip" @click="resetForm"
+ value="" :title="t('settings', 'Cancel and reset the form')">
+ </div>
+ </form>
+
+ <user-row v-for="(user, key) in users" :user="user" :key="key" :settings="settings" :showConfig="showConfig"
+ :groups="groups" :subAdminsGroups="subAdminsGroups" :quotaOptions="quotaOptions" />
+ <infinite-loading @infinite="infiniteHandler">
+ <span slot="spinner"><div class="users-icon-loading"></div></span>
+ <span slot="no-more"><div class="users-list-end">— {{t('settings', 'no more results')}} —</div></span>
+ </infinite-loading>
+ </div>
+</template>
+
+<script>
+import userRow from './userList/userRow';
+import Multiselect from 'vue-multiselect';
+import InfiniteLoading from 'vue-infinite-loading';
+
+export default {
+ name: 'userList',
+ props: ['users', 'showConfig'],
+ components: {
+ userRow,
+ Multiselect,
+ InfiniteLoading
+ },
+ data() {
+ let unlimitedQuota = {id:'none', label:t('settings', 'Unlimited')},
+ defaultQuota = {id:'default', label:t('settings', 'Default quota')};
+ return {
+ unlimitedQuota: unlimitedQuota,
+ defaultQuota: defaultQuota,
+ loading: false,
+ scrolled: false,
+ newUser: {
+ id:'',
+ displayName:'',
+ password:'',
+ mailAddress:'',
+ groups: [],
+ subAdminsGroups: [],
+ quota: defaultQuota
+ }
+ };
+ },
+ mounted() {
+ if (!this.settings.canChangePassword) {
+ OC.Notification.showTemporary(t('settings','Password change is disabled because the master key is disabled'));
+ }
+ },
+ computed: {
+ settings() {
+ return this.$store.getters.getServerData;
+ },
+ groups() {
+ // data provided php side + remove the disabled group
+ return this.$store.getters.getGroups.filter(group => group.id !== '_disabled');
+ },
+ subAdminsGroups() {
+ // data provided php side
+ return this.$store.getters.getServerData.subadmingroups;
+ },
+ 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;
+ },
+ },
+ methods: {
+ onScroll(event) {
+ this.scrolled = event.target.scrollTop>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));
+ return this.newUser.quota = {id: quota, label: quota};
+ }
+ // Default is unlimited
+ return this.newUser.quota = this.quotaOptions[0];
+ },
+
+ infiniteHandler($state) {
+ this.$store.dispatch('getUsers', {offset:this.usersOffset, limit:this.usersLimit})
+ .then((response) => {response?$state.loaded():$state.complete()});
+ },
+
+ resetForm () {
+ // revert form to original state
+ Object.assign(this.newUser, this.$options.data.call(this).newUser);
+ this.loading = false;
+ },
+ createUser() {
+ this.loading = true;
+ this.$store.dispatch('addUser', {
+ userid: this.newUser.id,
+ password: this.newUser.password,
+ email: this.newUser.mailAddress,
+ groups: this.newUser.groups.map(group => group.id)
+ }).then(() =>this.resetForm());
+ }
+ }
+}
+</script>
diff --git a/settings/src/components/userList/userRow.vue b/settings/src/components/userList/userRow.vue
new file mode 100644
index 00000000000..816b0a33fa3
--- /dev/null
+++ b/settings/src/components/userList/userRow.vue
@@ -0,0 +1,370 @@
+<template>
+ <div class="row">
+ <div class="avatar"><img 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>
+ <form class="displayName" :class="{'icon-loading-small': loading.displayName}" v-on:submit.prevent="updateDisplayName">
+ <input :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 type="submit" class="icon-confirm" value="" />
+ </form>
+ <form class="password" v-if="settings.canChangePassword" :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="groups" :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" :limitText="limitGroups"
+ :multiple="true" :taggable="true" :closeOnSelect="false"
+ @tag="createGroup" @select="addUserGroup" @remove="removeUserGroup">
+ </multiselect>
+ </div>
+ <div class="subadmins" v-if="subAdminsGroups.length>0" :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" :limitText="limitGroups"
+ :multiple="true" :closeOnSelect="false"
+ @select="addUserSubAdmin" @remove="removeUserSubAdmin">
+ <span slot="noResult">{{t('settings','No result')}}</span>
+ </multiselect>
+ </div>
+ <div 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"
+ :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="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" :title="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'">
+ <div class="icon-more" v-click-outside="hideMenu" @click="showMenu"></div>
+ <div class="popovermenu" :class="{ 'open': openedMenu }">
+ <popover-menu :menu="userActions" />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import popoverMenu from '../popoverMenu';
+import ClickOutside from 'vue-click-outside';
+import Multiselect from 'vue-multiselect';
+//import Multiselect from '../../../node_modules/vue-multiselect/src/index';
+
+export default {
+ name: 'userRow',
+ props: ['user', 'settings', 'groups', 'subAdminsGroups', 'quotaOptions', 'showConfig'],
+ components: {
+ popoverMenu,
+ Multiselect
+ },
+ directives: {
+ ClickOutside
+ },
+ mounted() {
+ // prevent click outside event with popupItem.
+ this.popupItem = this.$el;
+ },
+ data() {
+ return {
+ rand: parseInt(Math.random() * 1000),
+ openedMenu: false,
+ loading: {
+ all: false,
+ displayName: false,
+ password: false,
+ mailAddress: false,
+ groups: false,
+ subadmins: false,
+ quota: false
+ }
+ }
+ },
+ computed: {
+ /* USER POPOVERMENU ACTIONS */
+ userActions() {
+ return [{
+ icon: 'icon-delete',
+ text: t('settings','Delete user'),
+ action: 'deleteUser',
+ data: this.user.id
+ },{
+ 'icon': this.user.enabled ? 'icon-close' : 'icon-add',
+ 'text': this.user.enabled ? t('settings','Disable user') : t('settings','Enable user'),
+ 'action': 'enableDisableUser',
+ data: {userid: this.user.id, enabled: !this.user.enabled}
+ }]
+ },
+
+ /* 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;
+ },
+
+ /* QUOTA MANAGEMENT */
+ 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 === 0 || 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;
+ }
+ },
+ methods: {
+ /* MENU HANDLING */
+ showMenu () {
+ this.openedMenu = true;
+ },
+ 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 the limit text in the selected options
+ *
+ * @param {int} count elements left
+ * @returns {string}
+ */
+ limitGroups(count) {
+ return '+'+count;
+ },
+
+ /**
+ * 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
+ *
+ * @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});
+ });
+ return this.$store.getters.getGroups[this.groups.length];
+ },
+
+ /**
+ * Add user to group
+ *
+ * @param {object} group Group object
+ * @returns {Promise}
+ */
+ addUserGroup(group) {
+ 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) {
+ 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);
+ },
+
+ /**
+ * 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);
+ },
+
+
+ /**
+ * Validate quota string to make sure it's a valid human file size
+ *
+ * @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 === 0) {
+ return this.setUserQuota('none');
+ } else if (validQuota !== null) {
+ // unify format output
+ return this.setUserQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)));
+ }
+ // if no valid doo not change
+ return false;
+ }
+ }
+}
+</script>
diff --git a/settings/src/main.js b/settings/src/main.js
new file mode 100644
index 00000000000..a3ca283f584
--- /dev/null
+++ b/settings/src/main.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import { sync } from 'vuex-router-sync';
+import App from './App.vue';
+import router from './router';
+import store from './store';
+
+sync(store, router);
+
+// bind to window
+Vue.prototype.t = t;
+Vue.prototype.OC = OC;
+Vue.prototype.oc_userconfig = oc_userconfig;
+
+const app = new Vue({
+ router,
+ store,
+ render: h => h(App)
+}).$mount('#content');
+
+export { app, router, store }; \ No newline at end of file
diff --git a/settings/src/router.js b/settings/src/router.js
new file mode 100644
index 00000000000..ffbefda1016
--- /dev/null
+++ b/settings/src/router.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import Router from 'vue-router';
+import Users from './views/Users';
+
+Vue.use(Router);
+
+/*
+ * This is the list of routes where the vuejs app will
+ * take over php to provide data
+ * You need to forward the php routing (routes.php) to
+ * /settings/main.php, where the vue-router will ensure
+ * the proper route.
+ * ⚠️ Routes needs to match the php routes.
+ */
+
+export default new Router({
+ mode: 'history',
+ base: window.location.pathName,
+ routes: [{
+ path: '/settings/users',
+ component: Users
+ }]
+}); \ No newline at end of file
diff --git a/settings/src/store/api.js b/settings/src/store/api.js
new file mode 100644
index 00000000000..d67c77a5ff3
--- /dev/null
+++ b/settings/src/store/api.js
@@ -0,0 +1,50 @@
+import axios from 'axios';
+
+const requestToken = document.getElementsByTagName('head')[0].getAttribute('data-requesttoken');
+const tokenHeaders = { headers: { requesttoken: requestToken } };
+
+const sanitize = function(url) {
+ return url.replace(/\/$/, ''); // Remove last slash of url
+}
+
+export default {
+ requireAdmin() {
+ return new Promise(function(resolve, reject) {
+ setTimeout(reject, 5000); // automatically reject 5s if not ok
+ function waitForpassword() {
+ if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
+ setTimeout(waitForpassword, 500);
+ return;
+ }
+ resolve();
+ }
+ waitForpassword();
+ OC.PasswordConfirmation.requirePasswordConfirmation();
+ }).catch((error) => console.log('Required password not entered'));
+ },
+ get(url) {
+ return axios.get(sanitize(url), tokenHeaders)
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error));
+ },
+ post(url, data) {
+ return axios.post(sanitize(url), data, tokenHeaders)
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error));
+ },
+ patch(url, data) {
+ return axios.patch(sanitize(url), { data: data, headers: tokenHeaders.headers })
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error));
+ },
+ put(url, data) {
+ return axios.put(sanitize(url), data, tokenHeaders)
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error));
+ },
+ delete(url, data) {
+ return axios.delete(sanitize(url), { data: data, headers: tokenHeaders.headers })
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error));
+ }
+}; \ No newline at end of file
diff --git a/settings/src/store/index.js b/settings/src/store/index.js
new file mode 100644
index 00000000000..045b097d03f
--- /dev/null
+++ b/settings/src/store/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import users from './users'
+import settings from './settings'
+
+Vue.use(Vuex)
+
+const debug = process.env.NODE_ENV !== 'production'
+
+const mutations = {
+ API_FAILURE(state, error) {
+ console.log(state, error);
+ }
+}
+
+export default new Vuex.Store({
+ modules: {
+ users,
+ settings
+ },
+ strict: debug,
+
+ mutations
+})
diff --git a/settings/src/store/settings.js b/settings/src/store/settings.js
new file mode 100644
index 00000000000..43c13357a08
--- /dev/null
+++ b/settings/src/store/settings.js
@@ -0,0 +1,18 @@
+import api from './api';
+
+const state = {
+ serverData: {}
+};
+const mutations = {
+ setServerData(state, data) {
+ state.serverData = data;
+ }
+};
+const getters = {
+ getServerData(state) {
+ return state.serverData;
+ }
+}
+const actions = {}
+
+export default {state, mutations, getters, actions};
diff --git a/settings/src/store/users.js b/settings/src/store/users.js
new file mode 100644
index 00000000000..b992fdb10b1
--- /dev/null
+++ b/settings/src/store/users.js
@@ -0,0 +1,380 @@
+import api from './api';
+
+const orderGroups = function(groups, orderBy) {
+ /* const SORT_USERCOUNT = 1;
+ * const SORT_GROUPNAME = 2;
+ * https://github.com/nextcloud/server/blob/208e38e84e1a07a49699aa90dc5b7272d24489f0/lib/private/Group/MetaData.php#L34
+ */
+ if (orderBy === 1) {
+ return groups.sort((a, b) => a.usercount < b.usercount);
+ } else {
+ return groups.sort((a, b) => a.name.localeCompare(b.name));
+ }
+}
+
+const state = {
+ users: [],
+ groups: [],
+ orderBy: 1,
+ minPasswordLength: 0,
+ usersOffset: 0,
+ usersLimit: 25,
+};
+
+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;
+ },
+ setPasswordPolicyMinLength(state, length) {
+ state.minPasswordLength = length!=='' ? length : 0;
+ },
+ initGroups(state, {groups, orderBy}) {
+ state.groups = groups;
+ state.orderBy = orderBy;
+ state.groups = orderGroups(state.groups, state.orderBy);
+ },
+ addGroup(state, groupid) {
+ try {
+ state.groups.push({
+ id: groupid,
+ name: groupid,
+ usercount: 0 // user will be added after the creation
+ });
+ state.groups = orderGroups(state.groups, state.orderBy);
+ } catch (e) {
+ console.log('Can\'t create group', e);
+ }
+ },
+ addUserGroup(state, { userid, gid }) {
+ // this should not be needed as it would means the user contains a group
+ // the server database doesn't have.
+ let group = state.groups.find(groupSearch => groupSearch.id == gid);
+ if (group) {
+ group.usercount++; // increase count
+ }
+ let groups = state.users.find(user => user.id == userid).groups;
+ groups.push(gid);
+ state.groups = orderGroups(state.groups, state.orderBy);
+ },
+ removeUserGroup(state, { userid, gid }) {
+ // this should not be needed as it would means the user contains a group
+ // the server database doesn't have.
+ let group = state.groups.find(groupSearch => groupSearch.id == gid);
+ if (group) {
+ group.usercount--; // lower count
+ }
+ let groups = state.users.find(user => user.id == userid).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);
+ },
+ removeUserSubAdmin(state, { userid, gid }) {
+ 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);
+ },
+ addUserData(state, response) {
+ state.users.push(response.data.ocs.data);
+ },
+ enableDisableUser(state, { userid, enabled }) {
+ state.users.find(user => user.id == userid).enabled = enabled;
+ state.groups.find(group => group.id == '_disabled').usercount += 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?humanValue:value;
+ } else {
+ state.users.find(user => user.id == userid)[key] = value;
+ }
+ },
+};
+
+const getters = {
+ getUsers(state) {
+ return state.users;
+ },
+ getGroups(state) {
+ return state.groups;
+ },
+ getPasswordPolicyMinLength(state) {
+ return state.minPasswordLength;
+ },
+ getUsersOffset(state) {
+ return state.usersOffset;
+ },
+ getUsersLimit(state) {
+ return state.usersLimit;
+ }
+};
+
+const actions = {
+ /**
+ * Get all users with full details
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @param {int} options.offset List offset to request
+ * @param {int} options.limit List number to return from offset
+ * @returns {Promise}
+ */
+ getUsers(context, { offset, limit, 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;
+ }
+ return false;
+ })
+ .catch((error) => context.commit('API_FAILURE', error));
+ },
+
+ /**
+ * Get all users with full details
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @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 : '';
+ 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;
+ }
+ return false;
+ })
+ .catch((error) => context.commit('API_FAILURE', error));
+ },
+
+ /**
+ * Get all users with full details from a groupid
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @param {int} options.offset List offset to request
+ * @param {int} options.limit List number to return from offset
+ * @returns {Promise}
+ */
+ 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));
+ },
+
+
+ getPasswordPolicyMinLength(context) {
+ return api.get(OC.linkToOCS('apps/provisioning_api/api/v1/config/apps/password_policy/minLength', 2))
+ .then((response) => context.commit('setPasswordPolicyMinLength', response.data.ocs.data.data))
+ .catch((error) => context.commit('API_FAILURE', error));
+ },
+
+ /**
+ * Add group
+ *
+ * @param {Object} 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})
+ .then((response) => context.commit('addGroup', gid))
+ .catch((error) => context.commit('API_FAILURE', error));
+ });
+ },
+
+ /**
+ * Add group
+ *
+ * @param {Object} context
+ * @param {string} gid Group id
+ * @returns {Promise}
+ */
+ removeGroup(context, gid) {
+ return api.requireAdmin().then((response) => {
+ return api.post(OC.linkToOCS(`cloud/groups`, 2), {groupid: gid})
+ .then((response) => context.commit('removeGroup', gid))
+ .catch((error) => context.commit('API_FAILURE', error));
+ });
+ },
+
+ /**
+ * Add user to group
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @param {string} options.userid User id
+ * @param {string} options.gid Group id
+ * @returns {Promise}
+ */
+ addUserGroup(context, { userid, gid }) {
+ 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) => context.commit('API_FAILURE', error));
+ });
+ },
+
+ /**
+ * Remove user from group
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @param {string} options.userid User id
+ * @param {string} options.gid Group id
+ * @returns {Promise}
+ */
+ removeUserGroup(context, { userid, gid }) {
+ 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) => context.commit('API_FAILURE', { userid, error }));
+ });
+ },
+
+ /**
+ * Add user to group admin
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @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 })
+ .then((response) => context.commit('addUserSubAdmin', { userid, gid }))
+ .catch((error) => context.commit('API_FAILURE', error));
+ });
+ },
+
+ /**
+ * Remove user from group admin
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @param {string} options.userid User id
+ * @param {string} options.gid Group id
+ * @returns {Promise}
+ */
+ removeUserSubAdmin(context, { userid, gid }) {
+ 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) => context.commit('API_FAILURE', { userid, error }));
+ });
+ },
+
+ /**
+ * Delete a user
+ *
+ * @param {Object} 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) => context.commit('API_FAILURE', { userid, error }));
+ });
+ },
+
+ /**
+ * Add a user
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @param {string} options.userid User id
+ * @param {string} options.password User password
+ * @param {string} options.email User email
+ * @returns {Promise}
+ */
+ addUser({context, dispatch}, {userid, password, email, groups}) {
+ return api.requireAdmin().then((response) => {
+ return api.post(OC.linkToOCS(`cloud/users`, 2), {userid, password, email, groups})
+ .then((response) => dispatch('addUserData', userid))
+ .catch((error) => context.commit('API_FAILURE', { userid, error }));
+ });
+ },
+
+ /**
+ * Get user data and commit addition
+ *
+ * @param {Object} 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) => context.commit('API_FAILURE', { userid, error }));
+ });
+ },
+
+ /** Enable or disable user
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @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';
+ return api.requireAdmin().then((response) => {
+ return api.put(OC.linkToOCS(`cloud/users/${userid}/${userStatus}`, 2))
+ .then((response) => context.commit('enableDisableUser', { userid, enabled }))
+ .catch((error) => context.commit('API_FAILURE', { userid, error }));
+ });
+ },
+
+ /**
+ * Edit user data
+ *
+ * @param {Object} context
+ * @param {Object} options
+ * @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 }) {
+ if (['email', 'quota', 'displayname', 'password'].indexOf(key) !== -1) {
+ // We allow empty email or displayname
+ if (typeof value === 'string' &&
+ (
+ (['quota', 'password'].indexOf(key) !== -1 && value.length > 0) ||
+ ['email', 'displayname'].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) => context.commit('API_FAILURE', { userid, error }));
+ });
+ }
+ }
+ return Promise.reject(new Error('Invalid request data'));
+ }
+};
+
+export default { state, mutations, getters, actions }; \ No newline at end of file
diff --git a/settings/src/views/Users.vue b/settings/src/views/Users.vue
new file mode 100644
index 00000000000..59b72a92fb7
--- /dev/null
+++ b/settings/src/views/Users.vue
@@ -0,0 +1,152 @@
+<template>
+ <div id="app">
+ <app-navigation :menu="menu">
+ <template slot="settings-content">
+ <div>
+ <input type="checkbox" id="showLastLogin" class="checkbox"
+ :checked="showLastLogin" v-model="showLastLogin">
+ <label for="showLastLogin">{{t('settings', 'Show last login')}}</label>
+ </div>
+ <div>
+ <input type="checkbox" id="showUserBackend" class="checkbox"
+ :checked="showUserBackend" v-model="showUserBackend">
+ <label for="showUserBackend">{{t('settings', 'Show user backend')}}</label>
+ </div>
+ <div>
+ <input type="checkbox" id="showStoragePath" class="checkbox"
+ :checked="showStoragePath" v-model="showStoragePath">
+ <label for="showStoragePath">{{t('settings', 'Show storage path')}}</label>
+ </div>
+ </template>
+ </app-navigation>
+ <user-list :users="users" :showConfig="showConfig" />
+ </div>
+</template>
+
+<script>
+import appNavigation from '../components/appNavigation';
+import userList from '../components/userList';
+import Vue from 'vue';
+import VueLocalStorage from 'vue-localstorage'
+Vue.use(VueLocalStorage)
+
+export default {
+ name: 'Users',
+ components: {
+ appNavigation,
+ userList
+ },
+ beforeMount() {
+ this.$store.commit('initGroups', {
+ groups: this.$store.getters.getServerData.groups,
+ orderBy: this.$store.getters.getServerData.sortGroups
+ });
+ this.$store.dispatch('getPasswordPolicyMinLength');
+ },
+ data() {
+ return {
+ showConfig: {
+ showStoragePath: false,
+ showUserBackend: false,
+ showLastLogin: false,
+ showNewUserForm: false
+ }
+ }
+ },
+ methods: {
+ getLocalstorage(key) {
+ // force initialization
+ this.showConfig[key] = this.$localStorage.get(key) === 'true';
+ return this.showConfig[key];
+ },
+ setLocalStorage(key, status) {
+ this.showConfig[key] = status;
+ this.$localStorage.set(key, status);
+ return status;
+ }
+ },
+ computed: {
+ users() {
+ return this.$store.getters.getUsers;
+ },
+ loading() {
+ return Object.keys(this.users).length === 0;
+ },
+ usersOffset() {
+ return this.$store.getters.getUsersOffset;
+ },
+ usersLimit() {
+ return this.$store.getters.getUsersLimit;
+ },
+ showLastLogin: {
+ get: function() {return this.getLocalstorage('showLastLogin')},
+ set: function(status) {
+ this.setLocalStorage('showLastLogin', status);
+ }
+ },
+ showUserBackend: {
+ get: function() {return this.getLocalstorage('showUserBackend')},
+ set: function(status) {
+ this.setLocalStorage('showUserBackend', status);
+ }
+ },
+ showStoragePath: {
+ get: function() {return this.getLocalstorage('showStoragePath')},
+ set: function(status) {
+ this.setLocalStorage('showStoragePath', status);
+ }
+ },
+ menu() {
+ let self = this;
+ // Data provided php side
+ 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.classes = [];
+ item.href = '#group'+group.id.replace(' ', '_');
+ item.text = group.name;
+ item.utils = {counter: group.usercount};
+ return item;
+ });
+
+ // Adjust data
+ if (groups[0].id === 'admin') {
+ groups[0].text = t('settings', 'Admins');} // rename admin group
+ if (groups[1].id === '_disabled') {
+ groups[1].text = t('settings', 'Disabled users'); // rename disabled group
+ if (groups[1].utils.counter === 0) {
+ groups.splice(1, 1); // remove disabled if empty
+ }
+ }
+
+ // Add everyone group
+ groups.unshift({
+ id: '_everyone',
+ classes: ['active'],
+ href:'#group_everyone',
+ text: t('settings', 'Everyone'),
+ utils: {counter: this.users.length}
+ });
+
+ // Return
+ return {
+ id: 'usergrouplist',
+ new: {
+ id:'new-user-button',
+ text: t('settings','New user'),
+ icon: 'icon-add',
+ action: function(){self.showConfig.showNewUserForm=!self.showConfig.showNewUserForm}
+ },
+ items: groups
+ }
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+</style>