Add new group entry on users list + fixes

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2018-09-12 17:20:39 +02:00
parent a23d524098
commit 2b41b01bf2
No known key found for this signature in database
GPG key ID: 60C25B8C072916CF
12 changed files with 1517 additions and 298 deletions

View file

@ -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;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -1,54 +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>
<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>

View file

@ -1,155 +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>
<!-- 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>

View file

@ -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')"
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" :close-on-select="false"
:allowEmpty="settings.isAdmin">
: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];
}
}
}

View file

@ -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 });

View file

@ -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: {

View file

@ -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',

View file

@ -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'
],
},
{