mirror of
https://github.com/nextcloud/server.git
synced 2026-06-14 19:20:35 -04:00
Merge pull request #40719 from nextcloud/enh/a11y/semantic-user-table
This commit is contained in:
commit
649990ee8b
18 changed files with 394 additions and 280 deletions
|
|
@ -41,51 +41,44 @@
|
|||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<RecycleScroller v-else
|
||||
ref="scroller"
|
||||
class="user-list"
|
||||
<VirtualList v-else
|
||||
:data-component="UserRow"
|
||||
:data-sources="filteredUsers"
|
||||
data-key="id"
|
||||
:item-height="rowHeight"
|
||||
:style="style"
|
||||
: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"
|
||||
:extra-props="{
|
||||
users,
|
||||
settings,
|
||||
hasObfuscated,
|
||||
groups,
|
||||
subAdminsGroups,
|
||||
quotaOptions,
|
||||
languages,
|
||||
externalActions,
|
||||
}"
|
||||
@scroll-end="handleScrollEnd">
|
||||
<template #before>
|
||||
<caption class="hidden-visually">
|
||||
{{ t('settings', 'List of users. This list is not fully rendered for performance reasons. The users will be rendered as you navigate through the list.') }}
|
||||
</caption>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<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>
|
||||
<template #footer>
|
||||
<UserListFooter :loading="loading.users"
|
||||
:filtered-users="filteredUsers" />
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
</VirtualList>
|
||||
</Fragment>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
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'
|
||||
|
|
@ -94,6 +87,7 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
|||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import VirtualList from './Users/VirtualList.vue'
|
||||
import NewUserModal from './Users/NewUserModal.vue'
|
||||
import UserListFooter from './Users/UserListFooter.vue'
|
||||
import UserListHeader from './Users/UserListHeader.vue'
|
||||
|
|
@ -128,10 +122,9 @@ export default {
|
|||
NcIconSvgWrapper,
|
||||
NcLoadingIcon,
|
||||
NewUserModal,
|
||||
RecycleScroller,
|
||||
UserListFooter,
|
||||
UserListHeader,
|
||||
UserRow,
|
||||
VirtualList,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -147,6 +140,7 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
UserRow,
|
||||
loading: {
|
||||
all: false,
|
||||
groups: false,
|
||||
|
|
@ -295,16 +289,6 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
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')
|
||||
},
|
||||
|
||||
async handleScrollEnd() {
|
||||
await this.loadUsers()
|
||||
},
|
||||
|
|
@ -414,57 +398,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ export default Vue.extend({
|
|||
|
||||
&--loading {
|
||||
left: 0;
|
||||
min-width: var(--avatar-cell-width);
|
||||
width: var(--avatar-cell-width);
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
|
|
@ -119,6 +120,7 @@ export default Vue.extend({
|
|||
|
||||
&--count {
|
||||
left: var(--avatar-cell-width);
|
||||
min-width: var(--cell-width);
|
||||
width: var(--cell-width);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
scope="col">
|
||||
<span>{{ t('settings', 'Last login') }}</span>
|
||||
</th>
|
||||
<th class="header__cell header__cell--large"
|
||||
<th class="header__cell header__cell--large header__cell--fill"
|
||||
scope="col">
|
||||
<!-- TRANSLATORS This string describes a manager in the context of an organization -->
|
||||
<span>{{ t('settings', 'Manager') }}</span>
|
||||
|
|
|
|||
|
|
@ -24,31 +24,30 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Fragment>
|
||||
<tr class="user-list__row"
|
||||
:data-test="user.id">
|
||||
<td class="row__cell row__cell--avatar">
|
||||
<NcLoadingIcon v-if="isLoadingUser"
|
||||
:name="t('settings', 'Loading user …')"
|
||||
:size="32" />
|
||||
<NcAvatar v-else
|
||||
:key="user.id"
|
||||
<NcAvatar v-else-if="visible"
|
||||
disable-menu
|
||||
:show-user-status="false"
|
||||
:user="user.id" />
|
||||
</td>
|
||||
|
||||
<td class="row__cell row__cell--displayname"
|
||||
:data-test="user.id">
|
||||
<template v-if="idState.editing && user.backendCapabilities.setDisplayName">
|
||||
<td class="row__cell row__cell--displayname">
|
||||
<template v-if="editing && user.backendCapabilities.setDisplayName">
|
||||
<NcTextField ref="displayNameField"
|
||||
data-test="displayNameField"
|
||||
class="user-row-text-field"
|
||||
:trailing-button-label="t('settings', 'Submit')"
|
||||
:class="{ 'icon-loading-small': idState.loading.displayName }"
|
||||
:class="{ 'icon-loading-small': loading.displayName }"
|
||||
:show-trailing-button="true"
|
||||
:disabled="idState.loading.displayName || isLoadingField"
|
||||
:disabled="loading.displayName || isLoadingField"
|
||||
:label="t('settings', 'Change display name')"
|
||||
trailing-button-icon="arrowRight"
|
||||
:value.sync="idState.editedDisplayName"
|
||||
:value.sync="editedDisplayName"
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
|
|
@ -66,17 +65,17 @@
|
|||
|
||||
<td class="row__cell"
|
||||
:class="{ 'row__cell--obfuscated': hasObfuscated }">
|
||||
<template v-if="idState.editing && settings.canChangePassword && user.backendCapabilities.setPassword">
|
||||
<template v-if="editing && settings.canChangePassword && user.backendCapabilities.setPassword">
|
||||
<NcTextField class="user-row-text-field"
|
||||
:class="{'icon-loading-small': idState.loading.password}"
|
||||
:trailing-button-label="t('settings', 'Submit')"
|
||||
:class="{'icon-loading-small': loading.password}"
|
||||
:show-trailing-button="true"
|
||||
:disabled="idState.loading.password || isLoadingField"
|
||||
:disabled="loading.password || isLoadingField"
|
||||
:minlength="minPasswordLength"
|
||||
maxlength="469"
|
||||
:label="t('settings', 'Set new password')"
|
||||
trailing-button-icon="arrowRight"
|
||||
:value.sync="idState.editedPassword"
|
||||
:value.sync="editedPassword"
|
||||
autocapitalize="off"
|
||||
autocomplete="new-password"
|
||||
autocorrect="off"
|
||||
|
|
@ -91,15 +90,15 @@
|
|||
</td>
|
||||
|
||||
<td class="row__cell">
|
||||
<template v-if="idState.editing">
|
||||
<template v-if="editing">
|
||||
<NcTextField class="user-row-text-field"
|
||||
:class="{'icon-loading-small': idState.loading.mailAddress}"
|
||||
:class="{'icon-loading-small': loading.mailAddress}"
|
||||
:show-trailing-button="true"
|
||||
:trailing-button-label="t('settings', 'Submit')"
|
||||
:disabled="idState.loading.mailAddress || isLoadingField"
|
||||
:label="t('settings', 'Set new email address')"
|
||||
:disabled="loading.mailAddress || isLoadingField"
|
||||
trailing-button-icon="arrowRight"
|
||||
:value.sync="idState.editedMail"
|
||||
:value.sync="editedMail"
|
||||
autocapitalize="off"
|
||||
autocomplete="new-password"
|
||||
autocorrect="off"
|
||||
|
|
@ -114,7 +113,7 @@
|
|||
</td>
|
||||
|
||||
<td class="row__cell row__cell--large row__cell--multiline">
|
||||
<template v-if="idState.editing">
|
||||
<template v-if="editing">
|
||||
<label class="hidden-visually"
|
||||
:for="'groups' + uniqueId">
|
||||
{{ t('settings', 'Add user to group') }}
|
||||
|
|
@ -122,13 +121,12 @@
|
|||
<NcSelect :input-id="'groups' + uniqueId"
|
||||
:close-on-select="false"
|
||||
:disabled="isLoadingField"
|
||||
:loading="idState.loading.groups"
|
||||
:loading="loading.groups"
|
||||
:multiple="true"
|
||||
:options="availableGroups"
|
||||
:placeholder="t('settings', 'Add user to group')"
|
||||
:taggable="settings.isAdmin"
|
||||
:value="userGroups"
|
||||
class="select-vue"
|
||||
label="name"
|
||||
:no-wrap="true"
|
||||
:create-option="(value) => ({ name: value, isCreating: true })"
|
||||
|
|
@ -144,7 +142,7 @@
|
|||
|
||||
<td v-if="subAdminsGroups.length > 0 && settings.isAdmin"
|
||||
class="row__cell row__cell--large row__cell--multiline">
|
||||
<template v-if="idState.editing && settings.isAdmin && subAdminsGroups.length > 0">
|
||||
<template v-if="editing && settings.isAdmin && subAdminsGroups.length > 0">
|
||||
<label class="hidden-visually"
|
||||
:for="'subadmins' + uniqueId">
|
||||
{{ t('settings', 'Set user as admin for') }}
|
||||
|
|
@ -152,14 +150,13 @@
|
|||
<NcSelect :id="'subadmins' + uniqueId"
|
||||
:close-on-select="false"
|
||||
:disabled="isLoadingField"
|
||||
:loading="idState.loading.subadmins"
|
||||
:loading="loading.subadmins"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:no-wrap="true"
|
||||
:options="subAdminsGroups"
|
||||
:placeholder="t('settings', 'Set user as admin for')"
|
||||
:value="userSubAdminsGroups"
|
||||
class="select-vue"
|
||||
@option:deselected="removeUserSubAdmin"
|
||||
@option:selected="options => addUserSubAdmin(options.at(-1))" />
|
||||
</template>
|
||||
|
|
@ -170,7 +167,7 @@
|
|||
</td>
|
||||
|
||||
<td class="row__cell">
|
||||
<template v-if="idState.editing">
|
||||
<template v-if="editing">
|
||||
<label class="hidden-visually"
|
||||
:for="'quota' + uniqueId">
|
||||
{{ t('settings', 'Select user quota') }}
|
||||
|
|
@ -179,10 +176,9 @@
|
|||
:close-on-select="true"
|
||||
:create-option="validateQuota"
|
||||
:disabled="isLoadingField"
|
||||
:loading="idState.loading.quota"
|
||||
:loading="loading.quota"
|
||||
:clearable="false"
|
||||
:input-id="'quota' + uniqueId"
|
||||
class="select-vue"
|
||||
:options="quotaOptions"
|
||||
:placeholder="t('settings', 'Select user quota')"
|
||||
:taggable="true"
|
||||
|
|
@ -202,7 +198,7 @@
|
|||
<td v-if="showConfig.showLanguages"
|
||||
class="row__cell row__cell--large"
|
||||
data-test="language">
|
||||
<template v-if="idState.editing">
|
||||
<template v-if="editing">
|
||||
<label class="hidden-visually"
|
||||
:for="'language' + uniqueId">
|
||||
{{ t('settings', 'Set the language') }}
|
||||
|
|
@ -210,13 +206,12 @@
|
|||
<NcSelect :id="'language' + uniqueId"
|
||||
:allow-empty="false"
|
||||
:disabled="isLoadingField"
|
||||
:loading="idState.loading.languages"
|
||||
:loading="loading.languages"
|
||||
:clearable="false"
|
||||
:options="availableLanguages"
|
||||
:placeholder="t('settings', 'No language set')"
|
||||
:value="userLanguage"
|
||||
label="name"
|
||||
class="select-vue"
|
||||
@input="setUserLanguage" />
|
||||
</template>
|
||||
<span v-else-if="!isObfuscated">
|
||||
|
|
@ -243,21 +238,21 @@
|
|||
<span v-if="!isObfuscated">{{ userLastLogin }}</span>
|
||||
</td>
|
||||
|
||||
<td class="row__cell row__cell--large">
|
||||
<template v-if="idState.editing">
|
||||
<td class="row__cell row__cell--large row__cell--fill">
|
||||
<template v-if="editing">
|
||||
<label class="hidden-visually"
|
||||
:for="'manager' + uniqueId">
|
||||
{{ managerLabel }}
|
||||
</label>
|
||||
<NcSelect v-model="idState.currentManager"
|
||||
<NcSelect v-model="currentManager"
|
||||
class="select--fill"
|
||||
:input-id="'manager' + uniqueId"
|
||||
:close-on-select="true"
|
||||
:disabled="isLoadingField"
|
||||
:loading="idState.loadingPossibleManagers || idState.loading.manager"
|
||||
:loading="loadingPossibleManagers || loading.manager"
|
||||
label="displayname"
|
||||
:options="idState.possibleManagers"
|
||||
:options="possibleManagers"
|
||||
:placeholder="managerLabel"
|
||||
class="select-vue"
|
||||
@open="searchInitialUserManager"
|
||||
@search="searchUserManager"
|
||||
@option:selected="updateUserManager"
|
||||
|
|
@ -269,18 +264,16 @@
|
|||
</td>
|
||||
|
||||
<td class="row__cell row__cell--actions">
|
||||
<UserRowActions v-if="!isObfuscated && canEdit && !idState.loading.all"
|
||||
<UserRowActions v-if="visible && !isObfuscated && canEdit && !loading.all"
|
||||
:actions="userActions"
|
||||
:disabled="isLoadingField"
|
||||
:edit="idState.editing"
|
||||
:edit="editing"
|
||||
@update:edit="toggleEdit" />
|
||||
</td>
|
||||
</Fragment>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Fragment } from 'vue-frag'
|
||||
import { IdState } from 'vue-virtual-scroller'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { showSuccess, showError } from '@nextcloud/dialogs'
|
||||
|
||||
|
|
@ -299,7 +292,6 @@ export default {
|
|||
name: 'UserRow',
|
||||
|
||||
components: {
|
||||
Fragment,
|
||||
NcAvatar,
|
||||
NcLoadingIcon,
|
||||
NcProgressBar,
|
||||
|
|
@ -309,14 +301,6 @@ export default {
|
|||
},
|
||||
|
||||
mixins: [
|
||||
/**
|
||||
* Use scoped `idState` instead of `data` which is reused between rows
|
||||
*
|
||||
* See https://github.com/Akryum/vue-virtual-scroller/tree/v1/packages/vue-virtual-scroller#why-is-this-useful
|
||||
*/
|
||||
IdState({
|
||||
idProp: vm => vm.user.id,
|
||||
}),
|
||||
UserRowMixin,
|
||||
],
|
||||
|
||||
|
|
@ -325,6 +309,10 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
users: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
|
@ -359,7 +347,7 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
idState() {
|
||||
data() {
|
||||
return {
|
||||
selectedQuota: false,
|
||||
rand: Math.random().toString(36).substring(2),
|
||||
|
|
@ -402,15 +390,15 @@ export default {
|
|||
},
|
||||
|
||||
isLoadingUser() {
|
||||
return this.idState.loading.delete || this.idState.loading.disable || this.idState.loading.wipe
|
||||
return this.loading.delete || this.loading.disable || this.loading.wipe
|
||||
},
|
||||
|
||||
isLoadingField() {
|
||||
return this.idState.loading.delete || this.idState.loading.disable || this.idState.loading.all
|
||||
return this.loading.delete || this.loading.disable || this.loading.all
|
||||
},
|
||||
|
||||
uniqueId() {
|
||||
return this.user.id + this.idState.rand
|
||||
return this.user.id + this.rand
|
||||
},
|
||||
|
||||
userGroupsLabels() {
|
||||
|
|
@ -487,8 +475,8 @@ export default {
|
|||
// mapping saved values to objects
|
||||
editedUserQuota: {
|
||||
get() {
|
||||
if (this.idState.selectedQuota !== false) {
|
||||
return this.idState.selectedQuota
|
||||
if (this.selectedQuota !== false) {
|
||||
return this.selectedQuota
|
||||
}
|
||||
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
|
||||
|
|
@ -497,7 +485,7 @@ export default {
|
|||
return unlimitedQuota // unlimited
|
||||
},
|
||||
set(quota) {
|
||||
this.idState.selectedQuota = quota
|
||||
this.selectedQuota = quota
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -510,10 +498,6 @@ export default {
|
|||
if (this.user.manager) {
|
||||
await this.initManager(this.user.manager)
|
||||
}
|
||||
|
||||
// Reset loading state before mounting the component.
|
||||
// This is useful when we disable a user as the loading state cannot be properly reset upon promise resolution.
|
||||
Object.keys(this.idState.loading).forEach(key => (this.idState.loading[key] = false))
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -530,13 +514,13 @@ export default {
|
|||
},
|
||||
(result) => {
|
||||
if (result) {
|
||||
this.idState.loading.wipe = true
|
||||
this.idState.loading.all = true
|
||||
this.loading.wipe = true
|
||||
this.loading.all = true
|
||||
this.$store.dispatch('wipeUserDevices', userid)
|
||||
.then(() => showSuccess(t('settings', 'Wiped {userid}\'s devices', { userid })), { timeout: 2000 })
|
||||
.finally(() => {
|
||||
this.idState.loading.wipe = false
|
||||
this.idState.loading.all = false
|
||||
this.loading.wipe = false
|
||||
this.loading.all = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
@ -550,42 +534,42 @@ export default {
|
|||
|
||||
async initManager(userId) {
|
||||
await this.$store.dispatch('getUser', userId).then(response => {
|
||||
this.idState.currentManager = response?.data.ocs.data
|
||||
this.currentManager = response?.data.ocs.data
|
||||
})
|
||||
},
|
||||
|
||||
async searchInitialUserManager() {
|
||||
this.idState.loadingPossibleManagers = true
|
||||
this.loadingPossibleManagers = true
|
||||
await this.searchUserManager()
|
||||
this.idState.loadingPossibleManagers = false
|
||||
this.loadingPossibleManagers = false
|
||||
},
|
||||
|
||||
async searchUserManager(query) {
|
||||
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
|
||||
const users = response?.data ? this.filterManagers(Object.values(response?.data.ocs.data.users)) : []
|
||||
if (users.length > 0) {
|
||||
this.idState.possibleManagers = users
|
||||
this.possibleManagers = users
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async updateUserManager(manager) {
|
||||
if (manager === null) {
|
||||
this.idState.currentManager = ''
|
||||
this.currentManager = ''
|
||||
}
|
||||
this.idState.loading.manager = true
|
||||
this.loading.manager = true
|
||||
try {
|
||||
await this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'manager',
|
||||
value: this.idState.currentManager ? this.idState.currentManager.id : '',
|
||||
value: this.currentManager ? this.currentManager.id : '',
|
||||
})
|
||||
} catch (error) {
|
||||
// TRANSLATORS This string describes a manager in the context of an organization
|
||||
showError(t('setting', 'Failed to update user manager'))
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.idState.loading.manager = false
|
||||
this.loading.manager = false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -602,12 +586,12 @@ export default {
|
|||
},
|
||||
(result) => {
|
||||
if (result) {
|
||||
this.idState.loading.delete = true
|
||||
this.idState.loading.all = true
|
||||
this.loading.delete = true
|
||||
this.loading.all = true
|
||||
return this.$store.dispatch('deleteUser', userid)
|
||||
.then(() => {
|
||||
this.idState.loading.delete = false
|
||||
this.idState.loading.all = false
|
||||
this.loading.delete = false
|
||||
this.loading.all = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
@ -616,8 +600,8 @@ export default {
|
|||
},
|
||||
|
||||
enableDisableUser() {
|
||||
this.idState.loading.delete = true
|
||||
this.idState.loading.all = true
|
||||
this.loading.delete = true
|
||||
this.loading.all = true
|
||||
const userid = this.user.id
|
||||
const enabled = !this.user.enabled
|
||||
return this.$store.dispatch('enableDisableUser', {
|
||||
|
|
@ -625,8 +609,8 @@ export default {
|
|||
enabled,
|
||||
})
|
||||
.then(() => {
|
||||
this.idState.loading.delete = false
|
||||
this.idState.loading.all = false
|
||||
this.loading.delete = false
|
||||
this.loading.all = false
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -636,14 +620,14 @@ export default {
|
|||
* @param {string} displayName The display name
|
||||
*/
|
||||
updateDisplayName() {
|
||||
this.idState.loading.displayName = true
|
||||
this.loading.displayName = true
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'displayname',
|
||||
value: this.idState.editedDisplayName,
|
||||
value: this.editedDisplayName,
|
||||
}).then(() => {
|
||||
this.idState.loading.displayName = false
|
||||
if (this.idState.editedDisplayName === this.user.displayname) {
|
||||
this.loading.displayName = false
|
||||
if (this.editedDisplayName === this.user.displayname) {
|
||||
showSuccess(t('setting', 'Display name was successfully changed'))
|
||||
}
|
||||
})
|
||||
|
|
@ -655,18 +639,18 @@ export default {
|
|||
* @param {string} password The email address
|
||||
*/
|
||||
updatePassword() {
|
||||
this.idState.loading.password = true
|
||||
if (this.idState.editedPassword.length === 0) {
|
||||
this.loading.password = true
|
||||
if (this.editedPassword.length === 0) {
|
||||
showError(t('setting', "Password can't be empty"))
|
||||
this.idState.loading.password = false
|
||||
this.loading.password = false
|
||||
} else {
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'password',
|
||||
value: this.idState.editedPassword,
|
||||
value: this.editedPassword,
|
||||
}).then(() => {
|
||||
this.idState.loading.password = false
|
||||
this.idState.editedPassword = ''
|
||||
this.loading.password = false
|
||||
this.editedPassword = ''
|
||||
showSuccess(t('setting', 'Password was successfully changed'))
|
||||
})
|
||||
}
|
||||
|
|
@ -678,19 +662,19 @@ export default {
|
|||
* @param {string} mailAddress The email address
|
||||
*/
|
||||
updateEmail() {
|
||||
this.idState.loading.mailAddress = true
|
||||
if (this.idState.editedMail === '') {
|
||||
this.loading.mailAddress = true
|
||||
if (this.editedMail === '') {
|
||||
showError(t('setting', "Email can't be empty"))
|
||||
this.idState.loading.mailAddress = false
|
||||
this.idState.editedMail = this.user.email
|
||||
this.loading.mailAddress = false
|
||||
this.editedMail = this.user.email
|
||||
} else {
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'email',
|
||||
value: this.idState.editedMail,
|
||||
value: this.editedMail,
|
||||
}).then(() => {
|
||||
this.idState.loading.mailAddress = false
|
||||
if (this.idState.editedMail === this.user.email) {
|
||||
this.loading.mailAddress = false
|
||||
if (this.editedMail === this.user.email) {
|
||||
showSuccess(t('setting', 'Email was successfully changed'))
|
||||
}
|
||||
})
|
||||
|
|
@ -703,7 +687,7 @@ export default {
|
|||
* @param {string} gid Group id
|
||||
*/
|
||||
async createGroup({ name: gid }) {
|
||||
this.idState.loading = { groups: true, subadmins: true }
|
||||
this.loading = { groups: true, subadmins: true }
|
||||
try {
|
||||
await this.$store.dispatch('addGroup', gid)
|
||||
const userid = this.user.id
|
||||
|
|
@ -711,7 +695,7 @@ export default {
|
|||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.idState.loading = { groups: false, subadmins: false }
|
||||
this.loading = { groups: false, subadmins: false }
|
||||
}
|
||||
return this.$store.getters.getGroups[this.groups.length]
|
||||
},
|
||||
|
|
@ -727,7 +711,7 @@ export default {
|
|||
// Ignore
|
||||
return
|
||||
}
|
||||
this.idState.loading.groups = true
|
||||
this.loading.groups = true
|
||||
const userid = this.user.id
|
||||
const gid = group.id
|
||||
if (group.canAdd === false) {
|
||||
|
|
@ -738,7 +722,7 @@ export default {
|
|||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.idState.loading.groups = false
|
||||
this.loading.groups = false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -751,7 +735,7 @@ export default {
|
|||
if (group.canRemove === false) {
|
||||
return false
|
||||
}
|
||||
this.idState.loading.groups = true
|
||||
this.loading.groups = true
|
||||
const userid = this.user.id
|
||||
const gid = group.id
|
||||
try {
|
||||
|
|
@ -759,13 +743,13 @@ export default {
|
|||
userid,
|
||||
gid,
|
||||
})
|
||||
this.idState.loading.groups = false
|
||||
this.loading.groups = false
|
||||
// remove user from current list if current list is the removed group
|
||||
if (this.$route.params.selectedGroup === gid) {
|
||||
this.$store.commit('deleteUser', userid)
|
||||
}
|
||||
} catch {
|
||||
this.idState.loading.groups = false
|
||||
this.loading.groups = false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -775,7 +759,7 @@ export default {
|
|||
* @param {object} group Group object
|
||||
*/
|
||||
async addUserSubAdmin(group) {
|
||||
this.idState.loading.subadmins = true
|
||||
this.loading.subadmins = true
|
||||
const userid = this.user.id
|
||||
const gid = group.id
|
||||
try {
|
||||
|
|
@ -783,7 +767,7 @@ export default {
|
|||
userid,
|
||||
gid,
|
||||
})
|
||||
this.idState.loading.subadmins = false
|
||||
this.loading.subadmins = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
|
@ -795,7 +779,7 @@ export default {
|
|||
* @param {object} group Group object
|
||||
*/
|
||||
async removeUserSubAdmin(group) {
|
||||
this.idState.loading.subadmins = true
|
||||
this.loading.subadmins = true
|
||||
const userid = this.user.id
|
||||
const gid = group.id
|
||||
|
||||
|
|
@ -807,7 +791,7 @@ export default {
|
|||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.idState.loading.subadmins = false
|
||||
this.loading.subadmins = false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -822,7 +806,7 @@ export default {
|
|||
if (quota === 'none') {
|
||||
quota = unlimitedQuota
|
||||
}
|
||||
this.idState.loading.quota = true
|
||||
this.loading.quota = true
|
||||
// ensure we only send the preset id
|
||||
quota = quota.id ? quota.id : quota
|
||||
|
||||
|
|
@ -835,7 +819,7 @@ export default {
|
|||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.idState.loading.quota = false
|
||||
this.loading.quota = false
|
||||
}
|
||||
return quota
|
||||
},
|
||||
|
|
@ -868,7 +852,7 @@ export default {
|
|||
* @return {object}
|
||||
*/
|
||||
async setUserLanguage(lang) {
|
||||
this.idState.loading.languages = true
|
||||
this.loading.languages = true
|
||||
// ensure we only send the preset id
|
||||
try {
|
||||
await this.$store.dispatch('setUserData', {
|
||||
|
|
@ -876,7 +860,7 @@ export default {
|
|||
key: 'language',
|
||||
value: lang.code,
|
||||
})
|
||||
this.idState.loading.languages = false
|
||||
this.loading.languages = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
|
@ -887,24 +871,24 @@ export default {
|
|||
* Dispatch new welcome mail request
|
||||
*/
|
||||
sendWelcomeMail() {
|
||||
this.idState.loading.all = true
|
||||
this.loading.all = true
|
||||
this.$store.dispatch('sendWelcomeMail', this.user.id)
|
||||
.then(() => showSuccess(t('setting', 'Welcome mail sent!'), { timeout: 2000 }))
|
||||
.finally(() => {
|
||||
this.idState.loading.all = false
|
||||
this.loading.all = false
|
||||
})
|
||||
},
|
||||
|
||||
async toggleEdit() {
|
||||
this.idState.editing = !this.idState.editing
|
||||
if (this.idState.editing) {
|
||||
this.editing = !this.editing
|
||||
if (this.editing) {
|
||||
await this.$nextTick()
|
||||
this.$refs.displayNameField?.$refs?.inputField?.$refs?.input?.focus()
|
||||
}
|
||||
if (this.idState.editedDisplayName !== this.user.displayname) {
|
||||
this.idState.editedDisplayName = this.user.displayname
|
||||
} else if (this.idState.editedMail !== this.user.email) {
|
||||
this.idState.editedMail = this.user.email ?? ''
|
||||
if (this.editedDisplayName !== this.user.displayname) {
|
||||
this.editedDisplayName = this.user.displayname
|
||||
} else if (this.editedMail !== this.user.email) {
|
||||
this.editedMail = this.user.email ?? ''
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -914,6 +898,24 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
@import './shared/styles.scss';
|
||||
|
||||
.user-list__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);
|
||||
}
|
||||
}
|
||||
|
||||
// Limit width of select in fill cell
|
||||
.select--fill {
|
||||
max-width: calc(var(--cell-width-large) - (2 * var(--cell-padding)));
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
@include cell;
|
||||
|
||||
|
|
|
|||
199
apps/settings/src/components/Users/VirtualList.vue
Normal file
199
apps/settings/src/components/Users/VirtualList.vue
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
<!--
|
||||
- @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>
|
||||
<table class="user-list">
|
||||
<slot name="before" />
|
||||
|
||||
<thead ref="thead"
|
||||
role="rowgroup"
|
||||
class="user-list__header">
|
||||
<slot name="header" />
|
||||
</thead>
|
||||
|
||||
<tbody :style="tbodyStyle"
|
||||
class="user-list__body">
|
||||
<component :is="dataComponent"
|
||||
v-for="(item, i) in renderedItems"
|
||||
:key="item[dataKey]"
|
||||
:user="item"
|
||||
:visible="(i >= bufferItems || index <= bufferItems) && (i < shownItems - bufferItems)"
|
||||
v-bind="extraProps" />
|
||||
</tbody>
|
||||
|
||||
<tfoot ref="tfoot"
|
||||
v-element-visibility="handleFooterVisibility"
|
||||
role="rowgroup"
|
||||
class="user-list__footer">
|
||||
<slot name="footer" />
|
||||
</tfoot>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { vElementVisibility } from '@vueuse/components'
|
||||
import { debounce } from 'debounce'
|
||||
|
||||
import logger from '../../logger.js'
|
||||
|
||||
Vue.directive('elementVisibility', vElementVisibility)
|
||||
|
||||
// Items to render before and after the visible area
|
||||
const bufferItems = 3
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'VirtualList',
|
||||
|
||||
props: {
|
||||
dataComponent: {
|
||||
type: [Object, Function],
|
||||
required: true,
|
||||
},
|
||||
dataKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dataSources: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
itemHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
extraProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
bufferItems,
|
||||
index: 0,
|
||||
headerHeight: 0,
|
||||
tableHeight: 0,
|
||||
resizeObserver: null as ResizeObserver | null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
startIndex() {
|
||||
return Math.max(0, this.index - bufferItems)
|
||||
},
|
||||
|
||||
shownItems() {
|
||||
return Math.ceil((this.tableHeight - this.headerHeight) / this.itemHeight) + bufferItems * 2
|
||||
},
|
||||
|
||||
renderedItems() {
|
||||
return this.dataSources.slice(this.startIndex, this.startIndex + this.shownItems)
|
||||
},
|
||||
|
||||
tbodyStyle() {
|
||||
const isOverScrolled = this.startIndex + this.shownItems > this.dataSources.length
|
||||
const lastIndex = this.dataSources.length - this.startIndex - this.shownItems
|
||||
const hiddenAfterItems = Math.min(this.dataSources.length - this.startIndex, lastIndex)
|
||||
return {
|
||||
paddingTop: `${this.startIndex * this.itemHeight}px`,
|
||||
paddingBottom: isOverScrolled ? 0 : `${hiddenAfterItems * this.itemHeight}px`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const root = this.$el as HTMLElement
|
||||
const tfoot = this.$refs?.tfoot as HTMLElement
|
||||
const thead = this.$refs?.thead as HTMLElement
|
||||
|
||||
this.resizeObserver = new ResizeObserver(debounce(() => {
|
||||
this.headerHeight = thead?.clientHeight ?? 0
|
||||
this.tableHeight = root?.clientHeight ?? 0
|
||||
logger.debug('VirtualList resizeObserver updated')
|
||||
this.onScroll()
|
||||
}, 100, false))
|
||||
|
||||
this.resizeObserver.observe(root)
|
||||
this.resizeObserver.observe(tfoot)
|
||||
this.resizeObserver.observe(thead)
|
||||
|
||||
this.$el.addEventListener('scroll', this.onScroll)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleFooterVisibility(visible: boolean) {
|
||||
if (visible) {
|
||||
this.$emit('scroll-end')
|
||||
}
|
||||
},
|
||||
|
||||
onScroll() {
|
||||
// Max 0 to prevent negative index
|
||||
this.index = Math.max(0, Math.round(this.$el.scrollTop / this.itemHeight))
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-list {
|
||||
--avatar-cell-width: 48px;
|
||||
--cell-padding: 7px;
|
||||
--cell-width: 200px;
|
||||
--cell-width-large: 300px;
|
||||
--cell-min-width: calc(var(--cell-width) - (2 * var(--cell-padding)));
|
||||
|
||||
// Necessary for virtual scroll optimized rendering
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
&__header,
|
||||
&__footer {
|
||||
position: sticky;
|
||||
// Fix sticky positioning in Firefox
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__header {
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -21,8 +21,10 @@
|
|||
*/
|
||||
|
||||
@mixin row {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
height: var(--row-height);
|
||||
background-color: var(--color-main-background);
|
||||
}
|
||||
|
|
@ -33,6 +35,7 @@
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 var(--cell-padding);
|
||||
min-width: var(--cell-width);
|
||||
width: var(--cell-width);
|
||||
color: var(--color-main-text);
|
||||
|
||||
|
|
@ -64,6 +67,7 @@
|
|||
}
|
||||
|
||||
&--avatar {
|
||||
min-width: var(--avatar-cell-width);
|
||||
width: var(--avatar-cell-width);
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
|
|
@ -84,13 +88,21 @@
|
|||
}
|
||||
|
||||
&--large {
|
||||
width: 300px;
|
||||
min-width: var(--cell-width-large);
|
||||
width: var(--cell-width-large);
|
||||
}
|
||||
|
||||
&--obfuscated {
|
||||
min-width: 400px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
// Fill remaining row space with cell
|
||||
&--fill {
|
||||
min-width: var(--cell-width-large);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--actions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
|
|
@ -98,6 +110,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-width: 110px;
|
||||
width: 110px;
|
||||
background-color: var(--color-main-background);
|
||||
border-left: 1px solid var(--color-border);
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ describe('Settings: Create and delete users', function() {
|
|||
})
|
||||
|
||||
// see that the created user is in the list
|
||||
cy.get(`tbody.user-list__body tr td[data-test="john"]`).parents('tr').within(() => {
|
||||
cy.get('tbody.user-list__body tr[data-test="john"]').within(() => {
|
||||
// see that the list of users contains the user john
|
||||
cy.contains('john').should('exist')
|
||||
})
|
||||
|
|
@ -126,7 +126,7 @@ describe('Settings: Create and delete users', function() {
|
|||
})
|
||||
|
||||
// see that the created user is in the list
|
||||
cy.get(`tbody.user-list__body tr td[data-test="john"]`).parents('tr').within(() => {
|
||||
cy.get('tbody.user-list__body tr[data-test="john"]').within(() => {
|
||||
// see that the list of users contains the user john
|
||||
cy.contains('john').should('exist')
|
||||
})
|
||||
|
|
@ -139,7 +139,7 @@ describe('Settings: Create and delete users', function() {
|
|||
cy.reload().login(admin)
|
||||
|
||||
// see that the user is in the list
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
|
||||
// see that the list of users contains the user jdoe
|
||||
cy.contains(jdoe.userId).should('exist')
|
||||
// open the actions menu for the user
|
||||
|
|
@ -165,6 +165,6 @@ describe('Settings: Create and delete users', function() {
|
|||
})
|
||||
|
||||
// deleted clicked the user is not shown anymore
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').should('not.be.visible')
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@ describe('Settings: Show and hide columns', function() {
|
|||
|
||||
it('Can show a column', function() {
|
||||
// see that the language column is not in the header
|
||||
cy.get(`.user-list__header tr`).within(() => {
|
||||
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.get('tbody.user-list__body tr').each(($row) => {
|
||||
cy.wrap($row).get('[data-test="language"]').should('not.exist')
|
||||
})
|
||||
|
||||
|
|
@ -72,24 +72,24 @@ describe('Settings: Show and hide columns', function() {
|
|||
cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el)))
|
||||
|
||||
// see that the language column is in the header
|
||||
cy.get(`.user-list__header tr`).within(() => {
|
||||
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.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.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.get('tbody.user-list__body tr').each(($row) => {
|
||||
cy.wrap($row).get('[data-test="lastLogin"]').should('exist')
|
||||
})
|
||||
|
||||
|
|
@ -107,12 +107,12 @@ describe('Settings: Show and hide columns', function() {
|
|||
cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el)))
|
||||
|
||||
// see that the last login column is not in the header
|
||||
cy.get(`.user-list__header tr`).within(() => {
|
||||
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.get('tbody.user-list__body tr').each(($row) => {
|
||||
cy.wrap($row).get('[data-test="lastLogin"]').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -42,24 +42,24 @@ describe('Settings: Disable and enable users', function() {
|
|||
cy.enableUser(jdoe)
|
||||
|
||||
// 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(() => {
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).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('td.row__cell--actions button.action-item__menutoggle').click()
|
||||
cy.get('td.row__cell--actions button.action-item__menutoggle').click({ scrollBehavior: 'center' })
|
||||
})
|
||||
|
||||
// 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(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').should('not.be.visible')
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).should('not.exist')
|
||||
// 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(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').should('exist')
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).should('exist')
|
||||
})
|
||||
|
||||
it('Can enable the user', function() {
|
||||
|
|
@ -71,11 +71,11 @@ describe('Settings: Disable and enable users', function() {
|
|||
cy.url().should('match', /\/disabled/)
|
||||
|
||||
// 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(() => {
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).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('td.row__cell--actions button.action-item__menutoggle').click()
|
||||
cy.get('td.row__cell--actions button.action-item__menutoggle').click({ scrollBehavior: 'center' })
|
||||
})
|
||||
|
||||
// The "Enable user" action in the actions menu is shown and clicked
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ describe('Settings: Change user properties', function() {
|
|||
})
|
||||
|
||||
beforeEach(function() {
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
|
||||
// reset edit mode for the user jdoe
|
||||
cy.get('td.row__cell--actions .action-items > button:first-of-type')
|
||||
.invoke('attr', 'title')
|
||||
|
|
@ -51,14 +51,14 @@ describe('Settings: Change user properties', function() {
|
|||
})
|
||||
|
||||
it('Can change the display name', function() {
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).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(() => {
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
|
||||
// set the display name
|
||||
cy.get('input[data-test="displayNameField"]').should('exist').and('have.value', 'jdoe')
|
||||
cy.get('input[data-test="displayNameField"]').clear()
|
||||
|
|
@ -88,14 +88,14 @@ describe('Settings: Change user properties', function() {
|
|||
})
|
||||
|
||||
it('Can change the password', function() {
|
||||
cy.get(`tbody.user-list__body tr td[data-test="${jdoe.userId}"]`).parents('tr').within(() => {
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).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(() => {
|
||||
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).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
|
||||
|
|
|
|||
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/settings-users-8351.js
vendored
4
dist/settings-users-8351.js
vendored
File diff suppressed because one or more lines are too long
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
File diff suppressed because one or more lines are too long
32
package-lock.json
generated
32
package-lock.json
generated
|
|
@ -84,7 +84,6 @@
|
|||
"vue-multiselect": "^2.1.6",
|
||||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-virtual-scroller": "^1.1.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0",
|
||||
|
|
@ -21787,11 +21786,6 @@
|
|||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/scrollparent": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz",
|
||||
"integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA=="
|
||||
},
|
||||
"node_modules/select": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
|
||||
|
|
@ -25231,32 +25225,6 @@
|
|||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue-virtual-scroller": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-1.1.2.tgz",
|
||||
"integrity": "sha512-SkUyc7QHCJFB5h1Fya7LxVizlVzOZZuFVipBGHYoTK8dwLs08bIz/tclvRApYhksaJIm/nn51inzO2UjpGJPMQ==",
|
||||
"dependencies": {
|
||||
"scrollparent": "^2.0.1",
|
||||
"vue-observe-visibility": "^0.4.4",
|
||||
"vue-resize": "^0.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^2.6.11"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-virtual-scroller/node_modules/vue-observe-visibility": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz",
|
||||
"integrity": "sha512-xo0CEVdkjSjhJoDdLSvoZoQrw/H2BlzB5jrCBKGZNXN2zdZgMuZ9BKrxXDjNP2AxlcCoKc8OahI3F3r3JGLv2Q=="
|
||||
},
|
||||
"node_modules/vue-virtual-scroller/node_modules/vue-resize": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-0.4.5.tgz",
|
||||
"integrity": "sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==",
|
||||
"peerDependencies": {
|
||||
"vue": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue2-datepicker": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-3.11.1.tgz",
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@
|
|||
"vue-multiselect": "^2.1.6",
|
||||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-virtual-scroller": "^1.1.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue