mirror of
https://github.com/nextcloud/server.git
synced 2026-06-12 18:21:40 -04:00
Merge pull request #39050 from nextcloud/enh/a11y-users-table
This commit is contained in:
commit
41540ad4c3
39 changed files with 1568 additions and 1222 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1317,284 +1317,6 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
|
|||
opacity: .7;
|
||||
}
|
||||
|
||||
|
||||
/* USERS LIST -------------------------------------------------------------- */
|
||||
#body-settings {
|
||||
$grid-row-height: 60px;
|
||||
$grid-col-min-width: 160px;
|
||||
|
||||
#app-content.user-list-grid {
|
||||
display: grid;
|
||||
grid-column-gap: 20px;
|
||||
grid-auto-rows: minmax(60px, max-content);
|
||||
|
||||
.row {
|
||||
// TODO replace with css4 subgrid when available
|
||||
// fallback for ie11 no grid
|
||||
display: flex;
|
||||
display: grid;
|
||||
min-height: $grid-row-height;
|
||||
grid-row-start: span 1;
|
||||
grid-gap: 3px;
|
||||
align-items: center;
|
||||
/* let's define the column until storage path,
|
||||
what follows will be manually defined */
|
||||
grid-template-columns:
|
||||
44px
|
||||
minmax($grid-col-min-width + 30px, 1fr) // username, displayname
|
||||
minmax($grid-col-min-width, 1fr) // password
|
||||
minmax($grid-col-min-width, 1fr) // email
|
||||
minmax(1.5*$grid-col-min-width, 1fr) // groups
|
||||
minmax(1.5*$grid-col-min-width, 1fr) // group admins
|
||||
minmax($grid-col-min-width, 1fr) // quota
|
||||
minmax(1.5*$grid-col-min-width, 1fr) // manager
|
||||
repeat(auto-fit, minmax($grid-col-min-width, 1fr));
|
||||
border-bottom: var(--color-border) 1px solid;
|
||||
|
||||
&.disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
/* grid col width */
|
||||
.name,
|
||||
.password,
|
||||
.mailAddress,
|
||||
.languages,
|
||||
.storageLocation,
|
||||
.userBackend,
|
||||
.lastLogin {
|
||||
min-width: $grid-col-min-width;
|
||||
|
||||
doesnotexist:-o-prefocus, .strengthify-wrapper {
|
||||
color: var(--color-text-dark);
|
||||
vertical-align: baseline;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.row--editable) {
|
||||
&.name,
|
||||
&.password,
|
||||
&.displayName,
|
||||
&.mailAddress,
|
||||
&.userBackend,
|
||||
&.languages {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll if too much groups
|
||||
&:not(.row--editable) {
|
||||
.groups,
|
||||
.subadmins,
|
||||
.subAdminsGroups {
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.managers,
|
||||
.groups,
|
||||
.subadmins,
|
||||
.subAdminsGroups,
|
||||
.quota {
|
||||
min-width: $grid-col-min-width;
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
color: var(--color-text-dark);
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
progress {
|
||||
max-width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
.obfuscated {
|
||||
width: 400px;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.userActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
position: sticky;
|
||||
right: 0px;
|
||||
min-width: 88px;
|
||||
background-color: var(--color-main-background);
|
||||
}
|
||||
|
||||
&.row--editable .userActions {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--color-text-maxcontrast);
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* various */
|
||||
&#grid-header {
|
||||
position: sticky;
|
||||
align-self: normal;
|
||||
background-color: var(--color-main-background);
|
||||
z-index: 100; /* above multiselect */
|
||||
top: 0;
|
||||
|
||||
&.sticky {
|
||||
box-shadow: 0 -2px 10px 1px var(--color-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&#grid-header {
|
||||
color: var(--color-text-maxcontrast);
|
||||
border-bottom-width: thin;
|
||||
|
||||
#headerDisplayName,
|
||||
#headerPassword,
|
||||
#headerAddress,
|
||||
#headerGroups,
|
||||
#headerSubAdmins,
|
||||
#theHeaderUserBackend,
|
||||
#theHeaderLastLogin,
|
||||
#headerQuota,
|
||||
#theHeaderStorageLocation,
|
||||
#headerLanguages {
|
||||
/* Line up header text with column content for when there’s inputs */
|
||||
padding-left: 7px;
|
||||
text-transform: none;
|
||||
color: var(--color-text-maxcontrast);
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(#grid-header) {
|
||||
box-shadow: 5px 0 0 var(--color-primary-element) inset;
|
||||
}
|
||||
}
|
||||
|
||||
> form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> div,
|
||||
> .displayName > form,
|
||||
> form {
|
||||
grid-row: 1;
|
||||
display: inline-flex;
|
||||
color: var(--color-text-lighter);
|
||||
flex-grow: 1;
|
||||
|
||||
> input:not(:focus):not(:active) {
|
||||
border-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> input:focus, > input:active {
|
||||
+ .icon-confirm {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* inputs like mail, username, password */
|
||||
&:not(.userActions) > input:not([type='submit']) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&.name {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&.displayName,
|
||||
&.mailAddress {
|
||||
> input {
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.name,
|
||||
&.userBackend {
|
||||
/* better multi-line visual */
|
||||
line-height: 1.3em;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
/* not supported by all browsers
|
||||
so we keep the overflow hidden
|
||||
as a fallback */
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&.name .subtitle {
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
&.quota {
|
||||
display: flex;;
|
||||
justify-content: left;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
|
||||
progress {
|
||||
width: 150px;
|
||||
margin-top: 35px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-confirm {
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:active) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.avatar {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin: 6px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.userActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
// Make sure to cover whole row
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
padding-inline: 12px;
|
||||
background-color: var(--color-main-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.infinite-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
grid-row-start: span 4;
|
||||
}
|
||||
|
||||
.users-list-end {
|
||||
opacity: .5;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.animated {
|
||||
animation: blink-animation 1s steps(5, start) 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,120 +21,90 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div id="app-content"
|
||||
role="grid"
|
||||
:aria-label="t('settings', 'User\'s table')"
|
||||
class="user-list-grid"
|
||||
@scroll.passive="onScroll">
|
||||
<Fragment>
|
||||
<NewUserModal v-if="showConfig.showNewUserForm"
|
||||
:loading="loading"
|
||||
:new-user="newUser"
|
||||
:show-config="showConfig"
|
||||
@reset="resetForm"
|
||||
@close="showConfig.showNewUserForm = false" />
|
||||
<div id="grid-header"
|
||||
:class="{'sticky': scrolled && !showConfig.showNewUserForm}"
|
||||
class="row">
|
||||
<div id="headerAvatar" class="avatar" />
|
||||
<div id="headerName" class="name">
|
||||
<div class="subtitle">
|
||||
<strong>
|
||||
{{ t('settings', 'Display name') }}
|
||||
</strong>
|
||||
</div>
|
||||
{{ t('settings', 'Username') }}
|
||||
</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.showUserBackend || showConfig.showStoragePath"
|
||||
class="headerUserBackend userBackend">
|
||||
<div v-if="showConfig.showUserBackend" class="userBackend">
|
||||
{{ t('settings', 'User backend') }}
|
||||
</div>
|
||||
<div v-if="showConfig.showStoragePath"
|
||||
class="subtitle storageLocation">
|
||||
{{ t('settings', 'Storage location') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showConfig.showLastLogin"
|
||||
class="headerLastLogin lastLogin">
|
||||
{{ t('settings', 'Last login') }}
|
||||
</div>
|
||||
<div id="headerManager" class="manager">
|
||||
{{ t('settings', 'Manager') }}
|
||||
</div>
|
||||
<div class="userActions" />
|
||||
</div>
|
||||
|
||||
<UserRow v-for="user in filteredUsers"
|
||||
:key="user.id"
|
||||
:external-actions="externalActions"
|
||||
:groups="groups"
|
||||
:languages="languages"
|
||||
:quota-options="quotaOptions"
|
||||
:settings="settings"
|
||||
:show-config="showConfig"
|
||||
:sub-admins-groups="subAdminsGroups"
|
||||
:user="user"
|
||||
:users="users"
|
||||
:is-dark-theme="isDarkTheme" />
|
||||
@reset="resetForm"
|
||||
@close="closeModal" />
|
||||
|
||||
<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>
|
||||
<NcEmptyContent v-if="filteredUsers.length === 0"
|
||||
class="empty"
|
||||
:title="isInitialLoad && loading.users ? null : t('settings', 'No users')">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="isInitialLoad && loading.users"
|
||||
:title="t('settings', 'Loading users …')"
|
||||
:size="64" />
|
||||
<NcIconSvgWrapper v-else
|
||||
:svg="usersSvg" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<RecycleScroller v-else
|
||||
class="user-list"
|
||||
:style="style"
|
||||
ref="scroller"
|
||||
:items="filteredUsers"
|
||||
key-field="id"
|
||||
role="table"
|
||||
list-tag="tbody"
|
||||
list-class="user-list__body"
|
||||
item-tag="tr"
|
||||
item-class="user-list__row"
|
||||
:item-size="rowHeight"
|
||||
@hook:mounted="handleMounted"
|
||||
@scroll-end="handleScrollEnd">
|
||||
|
||||
<template #before>
|
||||
<caption class="hidden-visually">
|
||||
{{ t('settings', 'List of users. This list is not fully rendered for performances reasons. The users will be rendered as you navigate through the list.') }}
|
||||
</caption>
|
||||
<UserListHeader :has-obfuscated="hasObfuscated" />
|
||||
</template>
|
||||
|
||||
<template #default="{ item: user }">
|
||||
<UserRow :user="user"
|
||||
:users="users"
|
||||
:settings="settings"
|
||||
:has-obfuscated="hasObfuscated"
|
||||
:groups="groups"
|
||||
:sub-admins-groups="subAdminsGroups"
|
||||
:quota-options="quotaOptions"
|
||||
:languages="languages"
|
||||
:external-actions="externalActions" />
|
||||
</template>
|
||||
|
||||
<template #after>
|
||||
<UserListFooter :loading="loading.users"
|
||||
:filtered-users="filteredUsers" />
|
||||
</template>
|
||||
|
||||
</RecycleScroller>
|
||||
</Fragment>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
import { Fragment } from 'vue-frag'
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
|
||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
|
||||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import UserRow from './Users/UserRow.vue'
|
||||
import NewUserModal from './Users/NewUserModal.vue'
|
||||
import UserListFooter from './Users/UserListFooter.vue'
|
||||
import UserListHeader from './Users/UserListHeader.vue'
|
||||
import UserRow from './Users/UserRow.vue'
|
||||
|
||||
const unlimitedQuota = {
|
||||
id: 'none',
|
||||
label: t('settings', 'Unlimited'),
|
||||
}
|
||||
import { defaultQuota, isObfuscated, unlimitedQuota } from '../utils/userUtils.ts'
|
||||
import logger from '../logger.js'
|
||||
|
||||
const defaultQuota = {
|
||||
id: 'default',
|
||||
label: t('settings', 'Default quota'),
|
||||
}
|
||||
import usersSvg from '../../img/users.svg?raw'
|
||||
|
||||
const newUser = {
|
||||
id: '',
|
||||
|
|
@ -155,20 +125,18 @@ export default {
|
|||
name: 'UserList',
|
||||
|
||||
components: {
|
||||
InfiniteLoading,
|
||||
Fragment,
|
||||
NcEmptyContent,
|
||||
NcIconSvgWrapper,
|
||||
NcLoadingIcon,
|
||||
NewUserModal,
|
||||
RecycleScroller,
|
||||
UserListFooter,
|
||||
UserListHeader,
|
||||
UserRow,
|
||||
},
|
||||
|
||||
props: {
|
||||
users: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showConfig: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
selectedGroup: {
|
||||
type: String,
|
||||
default: null,
|
||||
|
|
@ -184,20 +152,39 @@ export default {
|
|||
loading: {
|
||||
all: false,
|
||||
groups: false,
|
||||
users: false,
|
||||
},
|
||||
scrolled: false,
|
||||
isInitialLoad: true,
|
||||
rowHeight: 55,
|
||||
usersSvg,
|
||||
searchQuery: '',
|
||||
newUser: Object.assign({}, newUser),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showConfig() {
|
||||
return this.$store.getters.getShowConfig
|
||||
},
|
||||
|
||||
settings() {
|
||||
return this.$store.getters.getServerData
|
||||
},
|
||||
selectedGroupDecoded() {
|
||||
return decodeURIComponent(this.selectedGroup)
|
||||
|
||||
style() {
|
||||
return {
|
||||
'--row-height': `${this.rowHeight}px`,
|
||||
}
|
||||
},
|
||||
|
||||
hasObfuscated() {
|
||||
return this.filteredUsers.some(user => isObfuscated(user))
|
||||
},
|
||||
|
||||
users() {
|
||||
return this.$store.getters.getUsers
|
||||
},
|
||||
|
||||
filteredUsers() {
|
||||
if (this.selectedGroup === 'disabled') {
|
||||
return this.users.filter(user => user.enabled === false)
|
||||
|
|
@ -208,16 +195,19 @@ export default {
|
|||
}
|
||||
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))
|
||||
},
|
||||
|
||||
subAdminsGroups() {
|
||||
// data provided php side
|
||||
return this.$store.getters.getSubadminGroups
|
||||
},
|
||||
|
||||
quotaOptions() {
|
||||
// convert the preset array into objects
|
||||
const quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({
|
||||
|
|
@ -231,12 +221,15 @@ export default {
|
|||
quotaPreset.unshift(defaultQuota)
|
||||
return quotaPreset
|
||||
},
|
||||
|
||||
usersOffset() {
|
||||
return this.$store.getters.getUsersOffset
|
||||
},
|
||||
|
||||
usersLimit() {
|
||||
return this.$store.getters.getUsersLimit
|
||||
},
|
||||
|
||||
usersCount() {
|
||||
return this.users.length
|
||||
},
|
||||
|
|
@ -254,37 +247,29 @@ export default {
|
|||
},
|
||||
]
|
||||
},
|
||||
isDarkTheme() {
|
||||
return window.getComputedStyle(this.$el)
|
||||
.getPropertyValue('--background-invert-if-dark') === 'invert(100%)'
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
// watch url change and group select
|
||||
selectedGroup(val, old) {
|
||||
async selectedGroup(val, old) {
|
||||
this.isInitialLoad = true
|
||||
// if selected is the disabled group but it's empty
|
||||
this.redirectIfDisabled()
|
||||
await this.redirectIfDisabled()
|
||||
this.$store.commit('resetUsers')
|
||||
this.$refs.infiniteLoading.stateChanger.reset()
|
||||
await this.loadUsers()
|
||||
this.setNewUserDefaultGroup(val)
|
||||
},
|
||||
|
||||
// make sure the infiniteLoading state is changed if we manually
|
||||
// add/remove data from the store
|
||||
usersCount(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()
|
||||
}
|
||||
filteredUsers(filteredUsers) {
|
||||
logger.debug(`${filteredUsers.length} filtered user(s)`)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
async created() {
|
||||
await this.loadUsers()
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
if (!this.settings.canChangePassword) {
|
||||
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
|
||||
}
|
||||
|
|
@ -303,40 +288,58 @@ export default {
|
|||
/**
|
||||
* If disabled group but empty, redirect
|
||||
*/
|
||||
this.redirectIfDisabled()
|
||||
await this.redirectIfDisabled()
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
unsubscribe('nextcloud:unified-search.search', this.search)
|
||||
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
|
||||
},
|
||||
|
||||
methods: {
|
||||
onScroll(event) {
|
||||
this.scrolled = event.target.scrollTo > 0
|
||||
async handleMounted() {
|
||||
// Add proper semantics to the recycle scroller slots
|
||||
const header = this.$refs.scroller.$refs.before
|
||||
const footer = this.$refs.scroller.$refs.after
|
||||
header.classList.add('user-list__header')
|
||||
header.setAttribute('role', 'rowgroup')
|
||||
footer.classList.add('user-list__footer')
|
||||
footer.setAttribute('role', 'rowgroup')
|
||||
},
|
||||
|
||||
infiniteHandler($state) {
|
||||
this.$store.dispatch('getUsers', {
|
||||
offset: this.usersOffset,
|
||||
limit: this.usersLimit,
|
||||
group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
|
||||
search: this.searchQuery,
|
||||
})
|
||||
.then((usersCount) => {
|
||||
if (usersCount > 0) {
|
||||
$state.loaded()
|
||||
}
|
||||
if (usersCount < this.usersLimit) {
|
||||
$state.complete()
|
||||
}
|
||||
async handleScrollEnd() {
|
||||
await this.loadUsers()
|
||||
},
|
||||
|
||||
async loadUsers() {
|
||||
this.loading.users = true
|
||||
try {
|
||||
await this.$store.dispatch('getUsers', {
|
||||
offset: this.usersOffset,
|
||||
limit: this.usersLimit,
|
||||
group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
|
||||
search: this.searchQuery,
|
||||
})
|
||||
logger.debug(`${this.users.length} total user(s) loaded`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to load users', { error })
|
||||
showError('Failed to load users')
|
||||
}
|
||||
this.loading.users = false
|
||||
this.isInitialLoad = false
|
||||
},
|
||||
|
||||
/* SEARCH */
|
||||
search({ query }) {
|
||||
closeModal() {
|
||||
this.$store.commit('setShowConfig', {
|
||||
key: 'showNewUserForm',
|
||||
value: false,
|
||||
})
|
||||
},
|
||||
|
||||
async search({ query }) {
|
||||
this.searchQuery = query
|
||||
this.$store.commit('resetUsers')
|
||||
this.$refs.infiniteLoading.stateChanger.reset()
|
||||
await this.loadUsers()
|
||||
},
|
||||
|
||||
resetSearch() {
|
||||
|
|
@ -384,15 +387,86 @@ export default {
|
|||
* 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() {
|
||||
async 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()
|
||||
await this.loadUsers()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './Users/shared/styles.scss';
|
||||
|
||||
.empty {
|
||||
:deep {
|
||||
.icon-vue {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
svg {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-list {
|
||||
--avatar-cell-width: 48px;
|
||||
--cell-padding: 7px;
|
||||
--cell-width: 200px;
|
||||
--cell-min-width: calc(var(--cell-width) - (2 * var(--cell-padding)));
|
||||
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
:deep {
|
||||
.user-list {
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
// Necessary for virtual scrolling absolute
|
||||
position: relative;
|
||||
margin-top: var(--row-height);
|
||||
}
|
||||
|
||||
&__row {
|
||||
@include row;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
|
||||
.row__cell:not(.row__cell--actions) {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vue-recycle-scroller__slot {
|
||||
&.user-list__header,
|
||||
&.user-list__footer {
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
&.user-list__header {
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&.user-list__footer {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -182,16 +182,6 @@ import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
|
|||
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
|
||||
|
||||
const unlimitedQuota = {
|
||||
id: 'none',
|
||||
label: t('settings', 'Unlimited'),
|
||||
}
|
||||
|
||||
const defaultQuota = {
|
||||
id: 'default',
|
||||
label: t('settings', 'Default quota'),
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'NewUserModal',
|
||||
|
||||
|
|
@ -214,8 +204,8 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
|
||||
showConfig: {
|
||||
type: Object,
|
||||
quotaOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -227,6 +217,10 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
showConfig() {
|
||||
return this.$store.getters.getShowConfig
|
||||
},
|
||||
|
||||
settings() {
|
||||
return this.$store.getters.getServerData
|
||||
},
|
||||
|
|
@ -265,20 +259,6 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
quotaOptions() {
|
||||
// convert the preset array into objects
|
||||
const quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({
|
||||
id: cur,
|
||||
label: cur,
|
||||
}), [])
|
||||
// add default presets
|
||||
if (this.settings.allowUnlimitedQuota) {
|
||||
quotaPreset.unshift(unlimitedQuota)
|
||||
}
|
||||
quotaPreset.unshift(defaultQuota)
|
||||
return quotaPreset
|
||||
},
|
||||
|
||||
languages() {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
126
apps/settings/src/components/Users/UserListFooter.vue
Normal file
126
apps/settings/src/components/Users/UserListFooter.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<!--
|
||||
- @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
-
|
||||
- @author Christopher Ng <chrng8@gmail.com>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- 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>
|
||||
<tr class="footer">
|
||||
<th scope="row">
|
||||
<span class="hidden-visually">{{ t('settings', 'Total rows summary') }}</span>
|
||||
</th>
|
||||
<td class="footer__cell footer__cell--loading">
|
||||
<NcLoadingIcon v-if="loading"
|
||||
:title="t('settings', 'Loading users …')"
|
||||
:size="32" />
|
||||
</td>
|
||||
<td class="footer__cell footer__cell--count footer__cell--multiline">
|
||||
<span aria-describedby="user-count-desc">{{ userCount }}</span>
|
||||
<span id="user-count-desc"
|
||||
class="hidden-visually">
|
||||
{{ t('settings', 'Scroll to load more rows') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
|
||||
import {
|
||||
translate as t,
|
||||
translatePlural as n,
|
||||
} from '@nextcloud/l10n'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'UserListFooter',
|
||||
|
||||
components: {
|
||||
NcLoadingIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
filteredUsers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
userCount(): string {
|
||||
if (this.loading) {
|
||||
return this.n(
|
||||
'settings',
|
||||
'{userCount} user …',
|
||||
'{userCount} users …',
|
||||
this.filteredUsers.length,
|
||||
{
|
||||
userCount: this.filteredUsers.length,
|
||||
},
|
||||
)
|
||||
}
|
||||
return this.n(
|
||||
'settings',
|
||||
'{userCount} user',
|
||||
'{userCount} users',
|
||||
this.filteredUsers.length,
|
||||
{
|
||||
userCount: this.filteredUsers.length,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
n,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './shared/styles.scss';
|
||||
|
||||
.footer {
|
||||
@include row;
|
||||
@include cell;
|
||||
|
||||
&__cell {
|
||||
position: sticky;
|
||||
color: var(--color-text-maxcontrast);
|
||||
|
||||
&--loading {
|
||||
left: 0;
|
||||
width: var(--avatar-cell-width);
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--count {
|
||||
left: var(--avatar-cell-width);
|
||||
width: var(--cell-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
150
apps/settings/src/components/Users/UserListHeader.vue
Normal file
150
apps/settings/src/components/Users/UserListHeader.vue
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<!--
|
||||
- @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
-
|
||||
- @author Christopher Ng <chrng8@gmail.com>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- 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>
|
||||
<tr class="header">
|
||||
<th class="header__cell header__cell--avatar"
|
||||
scope="col">
|
||||
<span class="hidden-visually">
|
||||
{{ t('settings', 'Avatar') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="header__cell header__cell--displayname"
|
||||
scope="col">
|
||||
<strong>
|
||||
{{ t('settings', 'Display name') }}
|
||||
</strong>
|
||||
<span class="header__subtitle">
|
||||
{{ t('settings', 'Username') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="header__cell"
|
||||
:class="{ 'header__cell--obfuscated': hasObfuscated }"
|
||||
scope="col">
|
||||
<span>{{ passwordLabel }}</span>
|
||||
</th>
|
||||
<th class="header__cell"
|
||||
scope="col">
|
||||
<span>{{ t('settings', 'Email') }}</span>
|
||||
</th>
|
||||
<th class="header__cell header__cell--large"
|
||||
scope="col">
|
||||
<span>{{ t('settings', 'Groups') }}</span>
|
||||
</th>
|
||||
<th v-if="subAdminsGroups.length > 0 && settings.isAdmin"
|
||||
class="header__cell header__cell--large"
|
||||
scope="col">
|
||||
<span>{{ t('settings', 'Group admin for') }}</span>
|
||||
</th>
|
||||
<th class="header__cell"
|
||||
scope="col">
|
||||
<span>{{ t('settings', 'Quota') }}</span>
|
||||
</th>
|
||||
<th v-if="showConfig.showLanguages"
|
||||
class="header__cell header__cell--large"
|
||||
scope="col">
|
||||
<span>{{ t('settings', 'Language') }}</span>
|
||||
</th>
|
||||
<th v-if="showConfig.showUserBackend || showConfig.showStoragePath"
|
||||
class="header__cell header__cell--large"
|
||||
scope="col">
|
||||
<span v-if="showConfig.showUserBackend">
|
||||
{{ t('settings', 'User backend') }}
|
||||
</span>
|
||||
<span v-if="showConfig.showStoragePath"
|
||||
class="header__subtitle">
|
||||
{{ t('settings', 'Storage location') }}
|
||||
</span>
|
||||
</th>
|
||||
<th v-if="showConfig.showLastLogin"
|
||||
class="header__cell"
|
||||
scope="col">
|
||||
<span>{{ t('settings', 'Last login') }}</span>
|
||||
</th>
|
||||
<th class="header__cell header__cell--large"
|
||||
scope="col">
|
||||
<span>{{ t('settings', 'Manager') }}</span>
|
||||
</th>
|
||||
<th class="header__cell header__cell--actions"
|
||||
scope="col">
|
||||
<span class="hidden-visually">
|
||||
{{ t('settings', 'User actions') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'UserListHeader',
|
||||
|
||||
props: {
|
||||
hasObfuscated: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
showConfig() {
|
||||
// @ts-expect-error: allow untyped $store
|
||||
return this.$store.getters.getShowConfig
|
||||
},
|
||||
|
||||
settings() {
|
||||
// @ts-expect-error: allow untyped $store
|
||||
return this.$store.getters.getServerData
|
||||
},
|
||||
|
||||
subAdminsGroups() {
|
||||
// @ts-expect-error: allow untyped $store
|
||||
return this.$store.getters.getSubadminGroups
|
||||
},
|
||||
|
||||
passwordLabel(): string {
|
||||
if (this.hasObfuscated) {
|
||||
return t('settings', 'Password or insufficient permissions message')
|
||||
}
|
||||
return t('settings', 'Password')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './shared/styles.scss';
|
||||
|
||||
.header {
|
||||
@include row;
|
||||
@include cell;
|
||||
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +1,44 @@
|
|||
<!--
|
||||
- @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @author Christopher Ng <chrng8@gmail.com>
|
||||
- @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- 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>
|
||||
<NcActions :aria-label="t('settings', 'Toggle user actions menu')"
|
||||
:disabled="disabled"
|
||||
:inline="1">
|
||||
<NcActionButton @click="toggleEdit">
|
||||
<NcActionButton :disabled="disabled"
|
||||
@click="toggleEdit">
|
||||
{{ edit ? t('settings', 'Done') : t('settings', 'Edit') }}
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :svg="editSvg" aria-hidden="true" />
|
||||
<NcIconSvgWrapper :key="editSvg" :svg="editSvg" aria-hidden="true" />
|
||||
</template>
|
||||
</NcActionButton>
|
||||
<NcActionButton v-for="(action, index) in actions"
|
||||
<NcActionButton v-for="({ action, icon, text }, index) in actions"
|
||||
:key="index"
|
||||
:aria-label="action.text"
|
||||
:icon="action.icon"
|
||||
@click="action.action">
|
||||
{{ action.text }}
|
||||
:disabled="disabled"
|
||||
:aria-label="text"
|
||||
:icon="icon"
|
||||
@click="action">
|
||||
{{ text }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</template>
|
||||
|
|
@ -48,6 +74,14 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* The state whether the row is currently disabled
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* The state whether the row is currently edited
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
<template>
|
||||
<div 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, isDarkTheme)">
|
||||
</div>
|
||||
<!-- dirty hack to ellipsis on two lines -->
|
||||
<div class="name">
|
||||
<div class="displayName subtitle">
|
||||
<div :title="user.displayname.length > 20 ? user.displayname : ''" class="cellText">
|
||||
<strong>
|
||||
{{ user.displayname }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
{{ user.id }}
|
||||
</div>
|
||||
<div />
|
||||
<div class="mailAddress">
|
||||
<div :title="user.email !== null && user.email.length > 20 ? user.email : ''" class="cellText">
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="groups">
|
||||
{{ userGroupsLabels }}
|
||||
</div>
|
||||
<div v-if="subAdminsGroups.length > 0 && settings.isAdmin" class="subAdminsGroups">
|
||||
{{ userSubAdminsGroupsLabels }}
|
||||
</div>
|
||||
<div class="userQuota">
|
||||
<div class="quota">
|
||||
{{ userQuota }} ({{ usedSpace }})
|
||||
<progress class="quota-user-progress"
|
||||
:class="{'warn': usedQuota > 80}"
|
||||
:value="usedQuota"
|
||||
max="100" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showConfig.showLanguages" class="languages">
|
||||
{{ userLanguage.name }}
|
||||
</div>
|
||||
<div v-if="showConfig.showUserBackend || showConfig.showStoragePath" class="userBackend">
|
||||
<div v-if="showConfig.showUserBackend" class="userBackend">
|
||||
{{ user.backend }}
|
||||
</div>
|
||||
<div v-if="showConfig.showStoragePath" :title="user.storageLocation" class="storageLocation subtitle">
|
||||
{{ user.storageLocation }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showConfig.showLastLogin" :title="userLastLoginTooltip" class="lastLogin">
|
||||
{{ userLastLogin }}
|
||||
</div>
|
||||
<div class="managers">
|
||||
{{ user.manager }}
|
||||
</div>
|
||||
<div class="userActions">
|
||||
<UserRowActions v-if="canEdit && !loading.all"
|
||||
:actions="userActions"
|
||||
:edit="false"
|
||||
@update:edit="toggleEdit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
|
||||
import UserRowActions from './UserRowActions.vue'
|
||||
import UserRowMixin from '../../mixins/UserRowMixin.js'
|
||||
|
||||
export default {
|
||||
name: 'UserRowSimple',
|
||||
components: {
|
||||
UserRowActions,
|
||||
},
|
||||
directives: {
|
||||
ClickOutside,
|
||||
},
|
||||
mixins: [UserRowMixin],
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showConfig: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
userActions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
openedMenu: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
subAdminsGroups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
settings: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isDarkTheme: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
userGroupsLabels() {
|
||||
return this.userGroups
|
||||
.map(group => group.name)
|
||||
.join(', ')
|
||||
},
|
||||
userSubAdminsGroupsLabels() {
|
||||
return this.userSubAdminsGroups
|
||||
.map(group => group.name)
|
||||
.join(', ')
|
||||
},
|
||||
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) })
|
||||
},
|
||||
canEdit() {
|
||||
return getCurrentUser().uid !== this.user.id || this.settings.isAdmin
|
||||
},
|
||||
userQuota() {
|
||||
let quota = this.user.quota.quota
|
||||
|
||||
if (quota === 'default') {
|
||||
quota = this.settings.defaultQuota
|
||||
if (quota !== 'none') {
|
||||
// convert to numeric value to match what the server would usually return
|
||||
quota = OC.Util.computerFileSize(quota)
|
||||
}
|
||||
}
|
||||
|
||||
// when the default quota is unlimited, the server returns -3 here, map it to "none"
|
||||
if (quota === 'none' || quota === -3) {
|
||||
return t('settings', 'Unlimited')
|
||||
} else if (quota >= 0) {
|
||||
return OC.Util.humanFileSize(quota)
|
||||
}
|
||||
return OC.Util.humanFileSize(0)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleMenu() {
|
||||
this.$emit('update:openedMenu', !this.openedMenu)
|
||||
},
|
||||
hideMenu() {
|
||||
this.$emit('update:openedMenu', false)
|
||||
},
|
||||
toggleEdit() {
|
||||
this.$emit('update:editing', true)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cellText {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.icon-more {
|
||||
background-color: var(--color-main-background);
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
110
apps/settings/src/components/Users/shared/styles.scss
Normal file
110
apps/settings/src/components/Users/shared/styles.scss
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
@mixin row {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
height: var(--row-height);
|
||||
background-color: var(--color-main-background);
|
||||
}
|
||||
|
||||
@mixin cell {
|
||||
&__cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 var(--cell-padding);
|
||||
width: var(--cell-width);
|
||||
color: var(--color-main-text);
|
||||
|
||||
strong,
|
||||
span,
|
||||
label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (min-width: 670px) { /* Show one &--large column between stickied columns */
|
||||
&--avatar,
|
||||
&--displayname {
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
background-color: var(--color-main-background);
|
||||
}
|
||||
|
||||
&--avatar {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&--displayname {
|
||||
left: var(--avatar-cell-width);
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
&--avatar {
|
||||
width: var(--avatar-cell-width);
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&--multiline {
|
||||
span {
|
||||
line-height: 1.3em;
|
||||
white-space: unset;
|
||||
|
||||
@supports (-webkit-line-clamp: 2) {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--large {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
&--obfuscated {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
&--actions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 110px;
|
||||
background-color: var(--color-main-background);
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,8 +22,6 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
user: {
|
||||
|
|
@ -46,10 +44,6 @@ export default {
|
|||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showConfig: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
languages: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
|
@ -60,6 +54,10 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
showConfig() {
|
||||
return this.$store.getters.getShowConfig
|
||||
},
|
||||
|
||||
/* GROUPS MANAGEMENT */
|
||||
userGroups() {
|
||||
const userGroups = this.groups.filter(group => this.user.groups.includes(group.id))
|
||||
|
|
@ -153,32 +151,4 @@ export default {
|
|||
return t('settings', 'Never')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Generate avatar url
|
||||
*
|
||||
* @param {string} user The user name
|
||||
* @param {bool} isDarkTheme Whether the avatar should be the dark version
|
||||
* @return {string}
|
||||
*/
|
||||
generateAvatar(user, isDarkTheme) {
|
||||
if (isDarkTheme) {
|
||||
return generateUrl(
|
||||
'/avatar/{user}/64/dark?v={version}',
|
||||
{
|
||||
user,
|
||||
version: oc_userconfig.avatar.version,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return generateUrl(
|
||||
'/avatar/{user}/64?v={version}',
|
||||
{
|
||||
user,
|
||||
version: oc_userconfig.avatar.version,
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,12 +62,22 @@ const state = {
|
|||
usersOffset: 0,
|
||||
usersLimit: 25,
|
||||
userCount: 0,
|
||||
showConfig: {
|
||||
showStoragePath: false,
|
||||
showUserBackend: false,
|
||||
showLastLogin: false,
|
||||
showNewUserForm: false,
|
||||
showLanguages: false,
|
||||
},
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
appendUsers(state, usersObj) {
|
||||
// convert obj to array
|
||||
const users = state.users.concat(Object.keys(usersObj).map(userid => usersObj[userid]))
|
||||
const existingUsers = state.users.map(({ id }) => id)
|
||||
const newUsers = Object.values(usersObj)
|
||||
.filter(({ id }) => !existingUsers.includes(id))
|
||||
|
||||
const users = state.users.concat(newUsers)
|
||||
state.usersOffset += state.usersLimit
|
||||
state.users = users
|
||||
},
|
||||
|
|
@ -149,7 +159,7 @@ const mutations = {
|
|||
},
|
||||
addUserData(state, response) {
|
||||
const user = response.data.ocs.data
|
||||
state.users.push(user)
|
||||
state.users.unshift(user)
|
||||
this.commit('updateUserCounts', { user, actionType: 'create' })
|
||||
},
|
||||
enableDisableUser(state, { userid, enabled }) {
|
||||
|
|
@ -221,6 +231,10 @@ const mutations = {
|
|||
state.users = []
|
||||
state.usersOffset = 0
|
||||
},
|
||||
|
||||
setShowConfig(state, { key, value }) {
|
||||
state.showConfig[key] = value
|
||||
},
|
||||
}
|
||||
|
||||
const getters = {
|
||||
|
|
@ -246,6 +260,9 @@ const getters = {
|
|||
getUserCount(state) {
|
||||
return state.userCount
|
||||
},
|
||||
getShowConfig(state) {
|
||||
return state.showConfig
|
||||
},
|
||||
}
|
||||
|
||||
const CancelToken = axios.CancelToken
|
||||
|
|
|
|||
40
apps/settings/src/utils/userUtils.ts
Normal file
40
apps/settings/src/utils/userUtils.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export const unlimitedQuota = {
|
||||
id: 'none',
|
||||
label: t('settings', 'Unlimited'),
|
||||
}
|
||||
|
||||
export const defaultQuota = {
|
||||
id: 'default',
|
||||
label: t('settings', 'Default quota'),
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `true` if the logged in user does not have permissions to view the
|
||||
* data of `user`
|
||||
*/
|
||||
export const isObfuscated = (user: { id: string, [key: string]: any }) => {
|
||||
const keys = Object.keys(user)
|
||||
return keys.length === 1 && keys.at(0) === 'id'
|
||||
}
|
||||
|
|
@ -129,9 +129,7 @@
|
|||
</template>
|
||||
</NcAppNavigation>
|
||||
<NcAppContent>
|
||||
<UserList :users="users"
|
||||
:show-config="showConfig"
|
||||
:selected-group="selectedGroupDecoded"
|
||||
<UserList :selected-group="selectedGroupDecoded"
|
||||
:external-actions="externalActions" />
|
||||
</NcAppContent>
|
||||
</NcContent>
|
||||
|
|
@ -160,6 +158,7 @@ import { generateUrl } from '@nextcloud/router'
|
|||
|
||||
import GroupListItem from '../components/GroupListItem.vue'
|
||||
import UserList from '../components/UserList.vue'
|
||||
import { unlimitedQuota } from '../utils/userUtils.ts'
|
||||
|
||||
Vue.use(VueLocalStorage)
|
||||
|
||||
|
|
@ -189,23 +188,17 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
// default quota is set to unlimited
|
||||
unlimitedQuota: { id: 'none', label: t('settings', 'Unlimited') },
|
||||
// temporary value used for multiselect change
|
||||
selectedQuota: false,
|
||||
externalActions: [],
|
||||
loadingAddGroup: false,
|
||||
loadingSendMail: false,
|
||||
showConfig: {
|
||||
showStoragePath: false,
|
||||
showUserBackend: false,
|
||||
showLastLogin: false,
|
||||
showNewUserForm: false,
|
||||
showLanguages: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showConfig() {
|
||||
return this.$store.getters.getShowConfig
|
||||
},
|
||||
selectedGroupDecoded() {
|
||||
return this.selectedGroup ? decodeURIComponent(this.selectedGroup) : null
|
||||
},
|
||||
|
|
@ -224,25 +217,33 @@ export default {
|
|||
|
||||
// Local settings
|
||||
showLanguages: {
|
||||
get() { return this.getLocalstorage('showLanguages') },
|
||||
get() {
|
||||
return this.getLocalstorage('showLanguages')
|
||||
},
|
||||
set(status) {
|
||||
this.setLocalStorage('showLanguages', status)
|
||||
},
|
||||
},
|
||||
showLastLogin: {
|
||||
get() { return this.getLocalstorage('showLastLogin') },
|
||||
get() {
|
||||
return this.getLocalstorage('showLastLogin')
|
||||
},
|
||||
set(status) {
|
||||
this.setLocalStorage('showLastLogin', status)
|
||||
},
|
||||
},
|
||||
showUserBackend: {
|
||||
get() { return this.getLocalstorage('showUserBackend') },
|
||||
get() {
|
||||
return this.getLocalstorage('showUserBackend')
|
||||
},
|
||||
set(status) {
|
||||
this.setLocalStorage('showUserBackend', status)
|
||||
},
|
||||
},
|
||||
showStoragePath: {
|
||||
get() { return this.getLocalstorage('showStoragePath') },
|
||||
get() {
|
||||
return this.getLocalstorage('showStoragePath')
|
||||
},
|
||||
set(status) {
|
||||
this.setLocalStorage('showStoragePath', status)
|
||||
},
|
||||
|
|
@ -261,7 +262,7 @@ export default {
|
|||
const quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({ id: cur, label: cur }), [])
|
||||
// add default presets
|
||||
if (this.settings.allowUnlimitedQuota) {
|
||||
quotaPreset.unshift(this.unlimitedQuota)
|
||||
quotaPreset.unshift(unlimitedQuota)
|
||||
}
|
||||
return quotaPreset
|
||||
},
|
||||
|
|
@ -271,11 +272,11 @@ export default {
|
|||
if (this.selectedQuota !== false) {
|
||||
return this.selectedQuota
|
||||
}
|
||||
if (this.settings.defaultQuota !== this.unlimitedQuota.id && OC.Util.computerFileSize(this.settings.defaultQuota) >= 0) {
|
||||
if (this.settings.defaultQuota !== 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 this.unlimitedQuota // unlimited
|
||||
return unlimitedQuota // unlimited
|
||||
},
|
||||
set(quota) {
|
||||
this.selectedQuota = quota
|
||||
|
|
@ -340,17 +341,20 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
showNewUserMenu() {
|
||||
this.showConfig.showNewUserForm = true
|
||||
this.$store.commit('setShowConfig', {
|
||||
key: 'showNewUserForm',
|
||||
value: true,
|
||||
})
|
||||
},
|
||||
getLocalstorage(key) {
|
||||
// force initialization
|
||||
const localConfig = this.$localStorage.get(key)
|
||||
// if localstorage is null, fallback to original values
|
||||
this.showConfig[key] = localConfig !== null ? localConfig === 'true' : this.showConfig[key]
|
||||
this.$store.commit('setShowConfig', { key, value: localConfig !== null ? localConfig === 'true' : this.showConfig[key] })
|
||||
return this.showConfig[key]
|
||||
},
|
||||
setLocalStorage(key, status) {
|
||||
this.showConfig[key] = status
|
||||
this.$store.commit('setShowConfig', { key, value: status })
|
||||
this.$localStorage.set(key, status)
|
||||
return status
|
||||
},
|
||||
|
|
@ -363,7 +367,7 @@ export default {
|
|||
setDefaultQuota(quota = 'none') {
|
||||
// Make sure correct label is set for unlimited quota
|
||||
if (quota === 'none') {
|
||||
quota = this.unlimitedQuota
|
||||
quota = unlimitedQuota
|
||||
}
|
||||
this.$store.dispatch('setAppConfig', {
|
||||
app: 'files',
|
||||
|
|
@ -391,7 +395,7 @@ export default {
|
|||
// only used for new presets sent through @Tag
|
||||
const validQuota = OC.Util.computerFileSize(quota)
|
||||
if (validQuota === null) {
|
||||
return this.unlimitedQuota
|
||||
return unlimitedQuota
|
||||
} else {
|
||||
// unify format output
|
||||
quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
|
||||
|
|
@ -485,6 +489,14 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-content {
|
||||
// Virtual list needs to be full height and is scrollable
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
// force hiding the editing action for the add group entry
|
||||
.app-navigation__list #addgroup::v-deep .app-navigation-entry__utils {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -24,29 +24,126 @@ import { User } from '@nextcloud/cypress'
|
|||
|
||||
const admin = new User('admin', 'admin')
|
||||
const jdoe = new User('jdoe', 'jdoe')
|
||||
const john = new User('john', '123456')
|
||||
|
||||
describe('Settings: Create and delete users', function() {
|
||||
before(function() {
|
||||
cy.login(admin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.deleteUser(jdoe)
|
||||
beforeEach(function() {
|
||||
cy.login(admin)
|
||||
cy.listUsers().then((users) => {
|
||||
cy.login(admin)
|
||||
if (users.includes('john')) {
|
||||
// ensure created user is deleted
|
||||
cy.deleteUser(john).login(admin)
|
||||
// ensure deleted user is not present
|
||||
cy.reload().login(admin)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Can create a user', function() {
|
||||
// open the New user modal
|
||||
cy.get('button#new-user-button').click()
|
||||
|
||||
cy.get('form[data-test="form"]').within(() => {
|
||||
// see that the username is ""
|
||||
cy.get('input[data-test="username"]').should('exist').and('have.value', '')
|
||||
// set the username to john
|
||||
cy.get('input[data-test="username"]').type('john')
|
||||
// see that the username is john
|
||||
cy.get('input[data-test="username"]').should('have.value', 'john')
|
||||
// see that the password is ""
|
||||
cy.get('input[type="password"]').should('exist').and('have.value', '')
|
||||
// set the password to 123456
|
||||
cy.get('input[type="password"]').type('123456')
|
||||
// see that the password is 123456
|
||||
cy.get('input[type="password"]').should('have.value', '123456')
|
||||
// submit the new user form
|
||||
cy.get('button[type="submit"]').click()
|
||||
})
|
||||
|
||||
// Ignore failure if modal is not shown
|
||||
cy.once('fail', (error) => {
|
||||
expect(error.name).to.equal('AssertionError')
|
||||
expect(error).to.have.property('node', '.modal-container')
|
||||
})
|
||||
// Make sure no confirmation modal is shown on top of the New user modal
|
||||
cy.get('body').find('.modal-container').then(($modals) => {
|
||||
if ($modals.length > 1) {
|
||||
cy.wrap($modals.first()).find('input[type="password"]').type(admin.password)
|
||||
cy.wrap($modals.first()).find('button').contains('Confirm').click()
|
||||
}
|
||||
})
|
||||
|
||||
// see that the created user is in the list
|
||||
cy.get(`tbody.user-list__body tr td[data-test="john"]`).parents('tr').within(() => {
|
||||
// see that the list of users contains the user john
|
||||
cy.contains('john').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can create a user with additional field data', function() {
|
||||
// open the New user modal
|
||||
cy.get('button#new-user-button').click()
|
||||
|
||||
cy.get('form[data-test="form"]').within(() => {
|
||||
// set the username
|
||||
cy.get('input[data-test="username"]').should('exist').and('have.value', '')
|
||||
cy.get('input[data-test="username"]').type('john')
|
||||
cy.get('input[data-test="username"]').should('have.value', 'john')
|
||||
// set the display name
|
||||
cy.get('input[data-test="displayName"]').should('exist').and('have.value', '')
|
||||
cy.get('input[data-test="displayName"]').type('John Smith')
|
||||
cy.get('input[data-test="displayName"]').should('have.value', 'John Smith')
|
||||
// set the email
|
||||
cy.get('input[data-test="email"]').should('exist').and('have.value', '')
|
||||
cy.get('input[data-test="email"]').type('john@example.org')
|
||||
cy.get('input[data-test="email"]').should('have.value', 'john@example.org')
|
||||
// set the password
|
||||
cy.get('input[type="password"]').should('exist').and('have.value', '')
|
||||
cy.get('input[type="password"]').type('123456')
|
||||
cy.get('input[type="password"]').should('have.value', '123456')
|
||||
// submit the new user form
|
||||
cy.get('button[type="submit"]').click()
|
||||
})
|
||||
|
||||
// Ignore failure if modal is not shown
|
||||
cy.once('fail', (error) => {
|
||||
expect(error.name).to.equal('AssertionError')
|
||||
expect(error).to.have.property('node', '.modal-container')
|
||||
})
|
||||
// Make sure no confirmation modal is shown on top of the New user modal
|
||||
cy.get('body').find('.modal-container').then(($modals) => {
|
||||
if ($modals.length > 1) {
|
||||
cy.wrap($modals.first()).find('input[type="password"]').type(admin.password)
|
||||
cy.wrap($modals.first()).find('button').contains('Confirm').click()
|
||||
}
|
||||
})
|
||||
|
||||
// see that the created user is in the list
|
||||
cy.get(`tbody.user-list__body tr td[data-test="john"]`).parents('tr').within(() => {
|
||||
// see that the list of users contains the user john
|
||||
cy.contains('john').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can delete a user', function() {
|
||||
// ensure user exists
|
||||
// create user
|
||||
cy.createUser(jdoe).login(admin)
|
||||
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
// ensure created user is present
|
||||
cy.reload().login(admin)
|
||||
|
||||
// see that the user is in the list
|
||||
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).within(() => {
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
// see that the list of users contains the user jdoe
|
||||
cy.contains(jdoe.userId).should('exist')
|
||||
// open the actions menu for the user
|
||||
cy.get('.userActions button.action-item__menutoggle').click()
|
||||
cy.get('td.row__cell--actions button.action-item__menutoggle').click()
|
||||
})
|
||||
|
||||
// The "Delete user" action in the actions menu is shown and clicked
|
||||
|
|
@ -54,6 +151,6 @@ describe('Settings: Create and delete users', function() {
|
|||
// And confirmation dialog accepted
|
||||
cy.get('.oc-dialog button').contains(`Delete ${jdoe.userId}`).click()
|
||||
// deleted clicked the user is not shown anymore
|
||||
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).should('not.exist')
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
116
cypress/e2e/settings/users_columns.cy.ts
Normal file
116
cypress/e2e/settings/users_columns.cy.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/cypress'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Settings: Show and hide columns', function() {
|
||||
before(function() {
|
||||
cy.login(admin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
// open the settings pane
|
||||
cy.get('.app-navigation button.settings-button').click()
|
||||
// reset all toggles
|
||||
cy.get('.app-navigation #app-settings__content input[type="checkbox"]').uncheck({ force: true })
|
||||
// enable the last login toggle
|
||||
cy.get('.app-navigation #app-settings__content').within(() => {
|
||||
cy.get('[data-test="showLastLogin"] input[type="checkbox"]').check({ force: true })
|
||||
})
|
||||
// close the settings pane
|
||||
cy.get('.app-navigation button.settings-button').click()
|
||||
})
|
||||
|
||||
it('Can show a column', function() {
|
||||
// see that the language column is not in the header
|
||||
cy.get(`.user-list__header tr`).within(() => {
|
||||
cy.contains('Language').should('not.exist')
|
||||
})
|
||||
|
||||
// see that the language column is not in all user rows
|
||||
cy.get(`tbody.user-list__body tr`).each(($row) => {
|
||||
cy.wrap($row).get('[data-test="language"]').should('not.exist')
|
||||
})
|
||||
|
||||
// open the settings pane
|
||||
cy.get('.app-navigation button.settings-button').click()
|
||||
|
||||
// enable the languages toggle
|
||||
cy.get('.app-navigation #app-settings__content').within(() => {
|
||||
cy.get('[data-test="showLanguages"] input[type="checkbox"]').should('not.be.checked')
|
||||
cy.get('[data-test="showLanguages"] input[type="checkbox"]').check({ force: true })
|
||||
cy.get('[data-test="showLanguages"] input[type="checkbox"]').should('be.checked')
|
||||
})
|
||||
|
||||
// close the settings pane
|
||||
cy.get('.app-navigation button.settings-button').click()
|
||||
|
||||
// see that the language column is in the header
|
||||
cy.get(`.user-list__header tr`).within(() => {
|
||||
cy.contains('Language').should('exist')
|
||||
})
|
||||
|
||||
// see that the language column is in all user rows
|
||||
cy.get(`tbody.user-list__body tr`).each(($row) => {
|
||||
cy.wrap($row).get('[data-test="language"]').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can hide a column', function() {
|
||||
// see that the last login column is in the header
|
||||
cy.get(`.user-list__header tr`).within(() => {
|
||||
cy.contains('Last login').should('exist')
|
||||
})
|
||||
|
||||
// see that the last login column is in all user rows
|
||||
cy.get(`tbody.user-list__body tr`).each(($row) => {
|
||||
cy.wrap($row).get('[data-test="lastLogin"]').should('exist')
|
||||
})
|
||||
|
||||
// open the settings pane
|
||||
cy.get('.app-navigation button.settings-button').click()
|
||||
|
||||
// disable the last login toggle
|
||||
cy.get('.app-navigation #app-settings__content').within(() => {
|
||||
cy.get('[data-test="showLastLogin"] input[type="checkbox"]').should('be.checked')
|
||||
cy.get('[data-test="showLastLogin"] input[type="checkbox"]').uncheck({ force: true })
|
||||
cy.get('[data-test="showLastLogin"] input[type="checkbox"]').should('not.be.checked')
|
||||
})
|
||||
|
||||
// close the settings pane
|
||||
cy.get('.app-navigation button.settings-button').click()
|
||||
|
||||
// see that the last login column is not in the header
|
||||
cy.get(`.user-list__header tr`).within(() => {
|
||||
cy.contains('Last login').should('not.exist')
|
||||
})
|
||||
|
||||
// see that the last login column is not in all user rows
|
||||
cy.get(`tbody.user-list__body tr`).each(($row) => {
|
||||
cy.wrap($row).get('[data-test="lastLogin"]').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -29,6 +29,8 @@ describe('Settings: Disable and enable users', function() {
|
|||
before(function() {
|
||||
cy.createUser(jdoe)
|
||||
cy.login(admin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
after(() => {
|
||||
|
|
@ -38,44 +40,42 @@ describe('Settings: Disable and enable users', function() {
|
|||
it('Can disable the user', function() {
|
||||
// ensure user is enabled
|
||||
cy.enableUser(jdoe)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
|
||||
// see that the user is in the list of active users
|
||||
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).within(() => {
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
// see that the list of users contains the user jdoe
|
||||
cy.contains(jdoe.userId).should('exist')
|
||||
// open the actions menu for the user
|
||||
cy.get('.userActions button.action-item__menutoggle').click()
|
||||
cy.get('td.row__cell--actions button.action-item__menutoggle').click()
|
||||
})
|
||||
|
||||
// The "Disable user" action in the actions menu is shown and clicked
|
||||
cy.get('.action-item__popper .action').contains('Disable user').should('exist').click()
|
||||
// When clicked the section is not shown anymore
|
||||
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).should('not.exist')
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').should('not.be.visible')
|
||||
// But the disabled user section now exists
|
||||
cy.get('#disabled').should('exist')
|
||||
// Open disabled users section
|
||||
cy.get('#disabled a').click()
|
||||
cy.url().should('match', /\/disabled/)
|
||||
// The list of disabled users should now contain the user
|
||||
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).should('exist')
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').should('exist')
|
||||
})
|
||||
|
||||
it('Can enable the user', function() {
|
||||
// ensure user is disabled
|
||||
cy.enableUser(jdoe, false)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
|
||||
// Open disabled users section
|
||||
cy.get('#disabled a').click()
|
||||
cy.url().should('match', /\/disabled/)
|
||||
|
||||
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).within(() => {
|
||||
// see that the user is in the list of active users
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
// see that the list of disabled users contains the user jdoe
|
||||
cy.contains(jdoe.userId).should('exist')
|
||||
// open the actions menu for the user
|
||||
cy.get('.userActions button.action-item__menutoggle').click()
|
||||
cy.get('td.row__cell--actions button.action-item__menutoggle').click()
|
||||
})
|
||||
|
||||
// The "Enable user" action in the actions menu is shown and clicked
|
||||
|
|
|
|||
|
|
@ -29,33 +29,80 @@ describe('Settings: Change user properties', function() {
|
|||
before(function() {
|
||||
cy.createUser(jdoe)
|
||||
cy.login(admin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
// reset edit mode for the user jdoe
|
||||
cy.get('td.row__cell--actions .action-items > button:first-of-type')
|
||||
.invoke('attr', 'title')
|
||||
.then((title) => {
|
||||
if (title === 'Done') {
|
||||
cy.get('td.row__cell--actions .action-items > button:first-of-type').click()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.deleteUser(jdoe)
|
||||
})
|
||||
|
||||
it('Can change the password', function() {
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
|
||||
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).within(($row) => {
|
||||
it('Can change the display name', function() {
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
// see that the list of users contains the user jdoe
|
||||
cy.contains(jdoe.userId).should('exist')
|
||||
// toggle the edit mode for the user jdoe
|
||||
cy.get('.userActions .action-items > button:first-of-type').click()
|
||||
cy.get('td.row__cell--actions .action-items > button:first-of-type').click()
|
||||
})
|
||||
|
||||
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).within(($row) => {
|
||||
// see that the edit mode is on
|
||||
cy.wrap($row).should('have.class', 'row--editable')
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
// set the display name
|
||||
cy.get('input[data-test="displayNameField"]').should('exist').and('have.value', 'jdoe')
|
||||
cy.get('input[data-test="displayNameField"]').clear()
|
||||
cy.get('input[data-test="displayNameField"]').type('John Doe')
|
||||
cy.get('input[data-test="displayNameField"]').should('have.value', 'John Doe')
|
||||
cy.get('input[data-test="displayNameField"] ~ button').click()
|
||||
|
||||
// Ignore failure if modal is not shown
|
||||
cy.once('fail', (error) => {
|
||||
expect(error.name).to.equal('AssertionError')
|
||||
expect(error).to.have.property('node', '.modal-container')
|
||||
})
|
||||
// Make sure no confirmation modal is shown
|
||||
cy.root().closest('body').find('.modal-container').then(($modal) => {
|
||||
if ($modal.length > 0) {
|
||||
cy.wrap($modal).find('input[type="password"]').type(admin.password)
|
||||
cy.wrap($modal).find('button').contains('Confirm').click()
|
||||
}
|
||||
})
|
||||
|
||||
// see that the display name cell is done loading
|
||||
cy.get('.user-row-text-field.icon-loading-small').should('exist')
|
||||
cy.waitUntil(() => cy.get('.user-row-text-field.icon-loading-small').should('not.exist'), { timeout: 10000 })
|
||||
})
|
||||
// Success message is shown
|
||||
cy.get('.toastify.toast-success').contains(/Display.+name.+was.+successfully.+changed/i).should('exist')
|
||||
})
|
||||
|
||||
it('Can change the password', function() {
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
// see that the list of users contains the user jdoe
|
||||
cy.contains(jdoe.userId).should('exist')
|
||||
// toggle the edit mode for the user jdoe
|
||||
cy.get('td.row__cell--actions .action-items > button:first-of-type').click()
|
||||
})
|
||||
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
// see that the password of user0 is ""
|
||||
cy.get('input[type="password"]').should('exist').and('have.value', '')
|
||||
// set the password for user0 to 123456
|
||||
cy.get('input[type="password"]').type('123456')
|
||||
// When I set the password for user0 to 123456
|
||||
cy.get('input[type="password"]').should('have.value', '123456')
|
||||
cy.get('.password button').click()
|
||||
cy.get('input[type="password"] ~ button').click()
|
||||
|
||||
// Ignore failure if modal is not shown
|
||||
cy.once('fail', (error) => {
|
||||
|
|
|
|||
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-users-8351.js
vendored
4
dist/settings-users-8351.js
vendored
File diff suppressed because one or more lines are too long
30
dist/settings-users-8351.js.LICENSE.txt
vendored
30
dist/settings-users-8351.js.LICENSE.txt
vendored
|
|
@ -1,9 +1,3 @@
|
|||
/*!
|
||||
* vue-infinite-loading v2.4.5
|
||||
* (c) 2016-2020 PeachScript
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
/*! For license information please see NcAppNavigationCaption.js.LICENSE.txt */
|
||||
|
||||
/*! For license information please see NcAppNavigationNew.js.LICENSE.txt */
|
||||
|
|
@ -11,27 +5,3 @@
|
|||
/*! For license information please see NcAppNavigationNewItem.js.LICENSE.txt */
|
||||
|
||||
/*! For license information please see NcAppNavigationSettings.js.LICENSE.txt */
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author Greta Doci <gretadoci@gmail.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
|
|
|||
2
dist/settings-users-8351.js.map
vendored
2
dist/settings-users-8351.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,6 +4,28 @@
|
|||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright 2022, Julia Kirschenheuter <julia.kirschenheuter@nextcloud.com>
|
||||
*
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-personal-info.js
vendored
4
dist/settings-vue-settings-personal-info.js
vendored
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 one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/workflowengine-workflowengine.js
vendored
4
dist/workflowengine-workflowengine.js
vendored
File diff suppressed because one or more lines are too long
2
dist/workflowengine-workflowengine.js.map
vendored
2
dist/workflowengine-workflowengine.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -82,7 +82,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function rowForUser($user) {
|
||||
return Locator::forThe()->css("div.user-list-grid div.row[data-id=$user]")->
|
||||
return Locator::forThe()->xpath("//tbody[contains(@class, 'user-list__body')]/tr[td[@data-test='$user']]")->
|
||||
describedAs("Row for user $user in Users Settings");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Feature: header
|
|||
And I click the New user button
|
||||
And I see that the new user form is shown
|
||||
And I create user user2 with password 123456acb
|
||||
And I see that the list of users contains the user user2
|
||||
# And I see that the list of users contains the user user2
|
||||
When I open the Contacts menu
|
||||
Then I see that the Contacts menu is shown
|
||||
And I see that the contact "user0" in the Contacts menu is shown
|
||||
|
|
@ -84,4 +84,3 @@ Feature: header
|
|||
Then I see that the no results message in the Contacts menu is shown
|
||||
And I see that the contact "user0" in the Contacts menu is not shown
|
||||
And I see that the contact "admin" in the Contacts menu is not shown
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Feature: login
|
|||
And I click the New user button
|
||||
And I see that the new user form is shown
|
||||
And I create user unknownUser with password 123456acb
|
||||
And I see that the list of users contains the user unknownUser
|
||||
# And I see that the list of users contains the user unknownUser
|
||||
And I act as John
|
||||
And I log in with user unknownUser and password 123456acb
|
||||
Then I see that the current page is the Files app
|
||||
|
|
|
|||
|
|
@ -1,71 +1,13 @@
|
|||
@apache
|
||||
Feature: users
|
||||
|
||||
Scenario: create a new user
|
||||
Given I act as Jane
|
||||
And I am logged in as the admin
|
||||
And I open the User settings
|
||||
And I click the New user button
|
||||
And I see that the new user form is shown
|
||||
When I create user unknownUser with password 123456acb
|
||||
Then I see that the list of users contains the user unknownUser
|
||||
|
||||
Scenario: create a new user with a custom display name
|
||||
Given I am logged in as the admin
|
||||
And I open the User settings
|
||||
When I click the New user button
|
||||
And I see that the new user form is shown
|
||||
And I set the user name for the new user to "test"
|
||||
And I set the display name for the new user to "Test display name"
|
||||
And I set the password for the new user to "123456acb"
|
||||
And I create the new user
|
||||
Then I see that the list of users contains the user "test"
|
||||
# And I see that the display name for the user "test" is "Test display name"
|
||||
|
||||
# Scenario: delete a user
|
||||
# Given I act as Jane
|
||||
# And I am logged in as the admin
|
||||
# And I open the User settings
|
||||
# And I see that the list of users contains the user user0
|
||||
# And I open the actions menu for the user user0
|
||||
# And I see that the "Delete user" action in the user0 actions menu is shown
|
||||
# When I click the "Delete user" action in the user0 actions menu
|
||||
# And I click the "Delete user0's account" button of the confirmation dialog
|
||||
# Then I see that the list of users does not contains the user user0
|
||||
|
||||
# Scenario: disable a user
|
||||
# Given I act as Jane
|
||||
# And I am logged in as the admin
|
||||
# And I open the User settings
|
||||
# And I see that the list of users contains the user user0
|
||||
# And I open the actions menu for the user user0
|
||||
# And I see that the "Disable user" action in the user0 actions menu is shown
|
||||
# When I click the "Disable user" action in the user0 actions menu
|
||||
# Then I see that the list of users does not contains the user user0
|
||||
# When I open the "Disabled users" section
|
||||
# Then I see that the list of users contains the user user0
|
||||
|
||||
# Scenario: users navigation without disabled users
|
||||
# Given I act as Jane
|
||||
# And I am logged in as the admin
|
||||
# And I open the User settings
|
||||
# And I open the "Disabled users" section
|
||||
# And I see that the list of users contains the user disabledUser
|
||||
# And I open the actions menu for the user disabledUser
|
||||
# And I see that the "Enable user" action in the disabledUser actions menu is shown
|
||||
# When I click the "Enable user" action in the disabledUser actions menu
|
||||
# Then I see that the section "Disabled users" is not shown
|
||||
# # check again after reloading the settings
|
||||
# When I open the User settings
|
||||
# Then I see that the section "Disabled users" is not shown
|
||||
|
||||
Scenario: assign user to a group
|
||||
Given I act as Jane
|
||||
And I am logged in as the admin
|
||||
And I open the User settings
|
||||
And I see that the list of users contains the user user0
|
||||
When I toggle the edit mode for the user user0
|
||||
Then I see that the edit mode is on for user user0
|
||||
# And I see that the list of users contains the user user0
|
||||
# When I toggle the edit mode for the user user0
|
||||
# Then I see that the edit mode is on for user user0
|
||||
# disabled because we need the TAB patch:
|
||||
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
|
||||
# When I assign the user user0 to the group admin
|
||||
|
|
@ -76,7 +18,7 @@ Feature: users
|
|||
Given I act as Jane
|
||||
And I am logged in as the admin
|
||||
And I open the User settings
|
||||
And I see that the list of users contains the user user0
|
||||
# And I see that the list of users contains the user user0
|
||||
# disabled because we need the TAB patch:
|
||||
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
|
||||
# And I assign the user user0 to the group Group1
|
||||
|
|
@ -101,44 +43,6 @@ Feature: users
|
|||
# When I click the "Yes" button of the confirmation dialog
|
||||
# Then I see that the section Group1 is not shown
|
||||
|
||||
Scenario: change columns visibility
|
||||
Given I act as Jane
|
||||
And I am logged in as the admin
|
||||
And I open the User settings
|
||||
And I open the settings
|
||||
And I see that the settings are opened
|
||||
When I toggle the showLanguages checkbox in the settings
|
||||
Then I see that the "Language" column is shown
|
||||
When I toggle the showLastLogin checkbox in the settings
|
||||
Then I see that the "Last login" column is shown
|
||||
When I toggle the showStoragePath checkbox in the settings
|
||||
Then I see that the "Storage location" column is shown
|
||||
When I toggle the showUserBackend checkbox in the settings
|
||||
Then I see that the "User backend" column is shown
|
||||
|
||||
# Scenario: change display name
|
||||
# Given I act as Jane
|
||||
# And I am logged in as the admin
|
||||
# And I open the User settings
|
||||
# And I see that the list of users contains the user user0
|
||||
# And I see that the displayName of user0 is user0
|
||||
# When I set the displayName for user0 to user1
|
||||
# And I see that the displayName cell for user user0 is done loading
|
||||
# Then I see that the displayName of user0 is user1
|
||||
|
||||
# Scenario: change password
|
||||
# Given I act as Jane
|
||||
# And I am logged in as the admin
|
||||
# And I open the User settings
|
||||
# And I see that the list of users contains the user user0
|
||||
# When I toggle the edit mode for the user user0
|
||||
# Then I see that the edit mode is on for user user0
|
||||
# And I see that the password of user0 is ""
|
||||
# When I set the password for user0 to 123456
|
||||
# And I see that the password cell for user user0 is done loading
|
||||
# # password input is emptied on change
|
||||
# Then I see that the password of user0 is ""
|
||||
|
||||
# Scenario: change email
|
||||
# Given I act as Jane
|
||||
# And I am logged in as the admin
|
||||
|
|
@ -153,10 +57,10 @@ Feature: users
|
|||
Given I act as Jane
|
||||
And I am logged in as the admin
|
||||
And I open the User settings
|
||||
And I see that the list of users contains the user user0
|
||||
When I toggle the edit mode for the user user0
|
||||
Then I see that the edit mode is on for user user0
|
||||
And I see that the user quota of user0 is Unlimited
|
||||
# And I see that the list of users contains the user user0
|
||||
# When I toggle the edit mode for the user user0
|
||||
# Then I see that the edit mode is on for user user0
|
||||
# And I see that the user quota of user0 is Unlimited
|
||||
# disabled because we need the TAB patch:
|
||||
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
|
||||
# When I set the user user0 quota to 1GB
|
||||
|
|
|
|||
Loading…
Reference in a new issue