Skip to content
Snippets Groups Projects
Unverified Commit b7bd6bd6 authored by Morris Jobke's avatar Morris Jobke Committed by GitHub
Browse files

Merge pull request #11193 from nextcloud/add-group-settings

Add new group entry on users list + fixes
parents 0ded277b 2b41b01b
No related branches found
No related tags found
No related merge requests found
......@@ -1377,21 +1377,6 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
/* USERS LIST -------------------------------------------------------------- */
#body-settings {
#app-navigation {
/* Hack to override the javascript orderBy */
#usergrouplist > li {
order: 4;
&#everyone {
order:1;
}
&#admin {
order:2;
}
&#disabled {
order:3;
}
}
}
$grid-row-height: 46px;
#app-content.user-list-grid {
display: grid;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -13,6 +13,7 @@
"dependencies": {
"axios": "^0.18.0",
"babel-polyfill": "^6.26.0",
"nextcloud-vue": "^0.1.2",
"v-tooltip": "^2.0.0-rc.33",
"vue": "^2.5.17",
"vue-click-outside": "^1.0.7",
......@@ -33,6 +34,8 @@
"babel-loader": "^8.0.2",
"css-loader": "^1.0.0",
"file-loader": "^1.1.11",
"node-sass": "^4.9.3",
"sass-loader": "^7.1.0",
"vue-loader": "^15.4.2",
"vue-template-compiler": "^2.5.17",
"webpack": "^4.19.1",
......
<!--
- @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-navigation" :class="{'icon-loading': menu.loading}">
<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 in menu.items" :item="item" :key="item.key" />
</ul>
<div id="app-settings" v-if="!!$slots['settings-content']">
<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>
<!--
- @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>
<!-- Is this a caption ? -->
<li class="app-navigation-caption" v-if="item.caption">{{item.text}}</li>
<!-- Navigation item -->
<nav-element v-else :id="item.id" v-bind="navElement(item)"
: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">
<img v-if="item.iconUrl" :alt="item.text" :src="item.iconUrl">
{{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 -->
<li v-if="item.utils.actions && item.utils.actions.length === 1"
class="app-navigation-entry-utils-menu-button">
<button @click="item.utils.actions[0].action" :class="item.utils.actions[0].icon" :title="item.utils.actions[0].text"></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 @click="action.action" :class="action.icon" :title="action.text"></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>
</nav-element>
</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'
);
},
// This is used to decide which outter element type to use
// li or router-link
navElement(item) {
if (item.href) {
return {
is: 'li'
};
}
return {
is: 'router-link',
tag: 'li',
to: item.router,
exact: true
};
}
},
mounted() {
// prevent click outside event with popupItem.
this.popupItem = this.$el;
}
};
</script>
......@@ -44,9 +44,9 @@
</div>
<form class="row" id="new-user" v-show="showConfig.showNewUserForm"
v-on:submit.prevent="createUser" :disabled="loading"
v-on:submit.prevent="createUser" :disabled="loading.all"
:class="{'sticky': scrolled && showConfig.showNewUserForm}">
<div :class="loading?'icon-loading-small':'icon-add'"></div>
<div :class="loading.all?'icon-loading-small':'icon-add'"></div>
<div class="name">
<input id="newusername" type="text" required v-model="newUser.id"
:placeholder="t('settings', 'Username')" name="username"
......@@ -74,12 +74,13 @@
<div class="groups">
<!-- hidden input trick for vanilla html5 form validation -->
<input type="text" :value="newUser.groups" v-if="!settings.isAdmin"
tabindex="-1" id="newgroups" :required="!settings.isAdmin" />
<multiselect :options="canAddGroups" 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"
:allowEmpty="settings.isAdmin">
tabindex="-1" id="newgroups" :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="createGroup">
<!-- If user is not admin, he is a subadmin.
Subadmins can't create users outside their groups
Therefore, empty select is forbidden -->
......@@ -154,7 +155,10 @@ export default {
return {
unlimitedQuota: unlimitedQuota,
defaultQuota: defaultQuota,
loading: false,
loading: {
all: false,
groups: false
},
scrolled: false,
searchQuery: '',
newUser: {
......@@ -318,10 +322,10 @@ export default {
resetForm() {
// revert form to original state
Object.assign(this.newUser, this.$options.data.call(this).newUser);
this.loading = false;
this.loading.all = false;
},
createUser() {
this.loading = true;
this.loading.all = true;
this.$store.dispatch('addUser', {
userid: this.newUser.id,
password: this.newUser.password,
......@@ -332,7 +336,7 @@ export default {
quota: this.newUser.quota.id,
language: this.newUser.language.code,
}).then(() => this.resetForm())
.catch(() => this.loading = false);
.catch(() => this.loading.all = false);
},
setNewUserDefaultGroup(value) {
if (value && value.length > 0) {
......@@ -345,6 +349,25 @@ export default {
}
// fallback, empty selected group
this.newUser.groups = [];
},
/**
* Create a new group
*
* @param {string} groups 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];
}
}
}
......
......@@ -296,7 +296,10 @@ const actions = {
addGroup(context, gid) {
return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`cloud/groups`, 2), {groupid: gid})
.then((response) => context.commit('addGroup', {gid: gid, displayName: gid}))
.then((response) => {
context.commit('addGroup', {gid: gid, displayName: gid})
return {gid: gid, displayName: gid}
})
.catch((error) => {throw error;});
}).catch((error) => {
context.commit('API_FAILURE', { gid, error });
......
......@@ -34,7 +34,7 @@
<script>
import appNavigation from '../components/appNavigation';
import { AppNavigation } from 'nextcloud-vue';
import appList from '../components/appList';
import Vue from 'vue';
import VueLocalStorage from 'vue-localstorage'
......@@ -42,7 +42,6 @@ import Multiselect from 'vue-multiselect';
import api from '../store/api';
import AppDetails from '../components/appDetails';
Vue.use(VueLocalStorage)
Vue.use(VueLocalStorage)
export default {
......@@ -59,7 +58,7 @@ export default {
},
components: {
AppDetails,
appNavigation,
AppNavigation,
appList,
},
methods: {
......
......@@ -57,21 +57,20 @@
</template>
<script>
import appNavigation from '../components/appNavigation';
import { AppNavigation } from 'nextcloud-vue';
import userList from '../components/userList';
import Vue from 'vue';
import VueLocalStorage from 'vue-localstorage'
import Multiselect from 'vue-multiselect';
import api from '../store/api';
Vue.use(VueLocalStorage)
Vue.use(VueLocalStorage)
export default {
name: 'Users',
props: ['selectedGroup'],
components: {
appNavigation,
AppNavigation,
userList,
Multiselect
},
......@@ -101,6 +100,8 @@ export default {
// temporary value used for multiselect change
selectedQuota: false,
externalActions: [],
showAddGroupEntry: false,
loadingAddGroup: false,
showConfig: {
showStoragePath: false,
showUserBackend: false,
......@@ -198,6 +199,26 @@ export default {
action: action
});
return this.externalActions;
},
/**
* Create a new group
*
* @param {Object} event The form submit event
* @returns {Promise}
*/
createGroup(event) {
let gid = event.target[0].value;
this.loadingAddGroup = true;
this.$store.dispatch('addGroup', gid)
.then(() => {
this.showAddGroupEntry = false;
this.loadingAddGroup = false;
})
.catch(() => {
this.loadingAddGroup = false;
});
return this.$store.getters.getGroups[this.groups.length];
}
},
computed: {
......@@ -276,6 +297,7 @@ export default {
// BUILD APP NAVIGATION MENU OBJECT
menu() {
// Data provided php side
let self = this;
let groups = this.$store.getters.getGroups;
groups = Array.isArray(groups) ? groups : [];
......@@ -302,31 +324,19 @@ export default {
if (item.id !== 'admin' && item.id !== 'disabled' && this.settings.isAdmin) {
// add delete button on real groups
let self = this;
item.utils.actions = [{
icon: 'icon-delete',
text: t('settings', 'Remove group'),
action: function() {self.removeGroup(group.id)}
action: function() {
self.removeGroup(group.id)
}
}];
};
return item;
});
// Adjust data
let adminGroup = groups.find(group => group.id == 'admin');
let disabledGroupIndex = groups.findIndex(group => group.id == 'disabled');
let disabledGroup = groups[disabledGroupIndex];
if (adminGroup && adminGroup.text) {
adminGroup.text = t('settings', 'Admins'); // rename admin group
adminGroup.icon = 'icon-user-admin'; // set icon
}
if (disabledGroup && disabledGroup.text) {
disabledGroup.text = t('settings', 'Disabled users'); // rename disabled group
disabledGroup.icon = 'icon-disabled-users'; // set icon
if (!disabledGroup.utils.counter) {
groups.splice(disabledGroupIndex, 1); // remove disabled if empty
}
}
// 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'});
......@@ -340,6 +350,26 @@ export default {
groups.unshift(separator);
}
// Adjust admin and disabled groups
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);
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
}
if (disabledGroup && disabledGroup.text) {
disabledGroup.text = t('settings', 'Disabled users'); // rename disabled group
disabledGroup.icon = 'icon-disabled-users'; // set icon
if (disabledGroup.utils && disabledGroup.utils.counter > 0) {
groups.unshift(disabledGroup); // add disabled if not empty
}
}
// Add everyone group
let everyoneGroup = {
......@@ -351,10 +381,35 @@ export default {
};
// users count
if (this.userCount > 0) {
everyoneGroup.utils = {counter: this.userCount};
Vue.set(everyoneGroup, 'utils', {
counter: this.userCount
});
}
groups.unshift(everyoneGroup);
let addGroup = {
id: 'addgroup',
key: 'addgroup',
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'),
action: this.createGroup,
reset: function() {
self.showAddGroupEntry = false
}
});
addGroup.classes = 'editing';
} else {
Vue.set(addGroup, 'action', function() {
self.showAddGroupEntry = true
})
}
groups.unshift(addGroup);
// Return
return {
id: 'usergrouplist',
......
......@@ -13,14 +13,13 @@ module.exports = {
{
test: /\.css$/,
use: [
'css-loader'
'vue-style-loader', 'css-loader'
],
},
{
test: /\.scss$/,
use: [
'css-loader',
'sass-loader'
'vue-style-loader', 'css-loader', 'sass-loader'
],
},
{
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment