Merge pull request #39073 from nextcloud/fix/setting_migrate-from-deprecated-ncpopovermenu

pull/39094/head
Pytal 2023-06-30 15:51:15 +07:00 committed by GitHub
commit cb118d5bd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 389 additions and 209 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1386,7 +1386,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
// Scroll if too much groups
&:not(.row--editable) {
.groups,
.subadmins,
.subadmins,
.subAdminsGroups {
overflow: auto;
max-height: 100%;
@ -1395,7 +1395,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
.managers,
.groups,
.subadmins,
.subadmins,
.subAdminsGroups,
.quota {
min-width: $grid-col-min-width;
@ -1569,50 +1569,14 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
&.userActions {
display: flex;
align-items: center;
justify-content: flex-end;
#newsubmit {
width: 100%;
}
.toggleUserActions {
position: relative;
display: flex;
align-items: center;
background-color: var(--color-main-background);
.icon-more {
width: 44px;
height: 44px;
opacity: .5;
cursor: pointer;
&:focus,
&:hover,
&:active {
opacity: .7;
background-color: var(--color-background-dark)
}
}
}
.feedback {
display: flex;
align-items: center;
white-space: nowrap;
transition: opacity 200ms ease-in-out;
.icon-checkmark {
opacity: .5;
margin-right: 5px;
}
}
}
/* Fill the grid cell */
.v-select.select-vue {
min-width: 100%;
width: 100%;
// Make sure to cover whole row
height: 100%;
width: fit-content;
padding-inline: 12px;
background-color: var(--color-main-background);
}
}
}

@ -44,7 +44,6 @@
<!-- User full data -->
<UserRowSimple v-else-if="!editing"
:editing.sync="editing"
:feedback-message="feedbackMessage"
:groups="groups"
:languages="languages"
:loading="loading"
@ -222,59 +221,41 @@
</div>
<div class="userActions">
<div v-if="!loading.all"
class="toggleUserActions">
<NcActions>
<NcActionButton icon="icon-checkmark"
:title="t('settings', 'Done')"
:aria-label="t('settings', 'Done')"
@click="handleDoneButton" />
</NcActions>
<div v-click-outside="hideMenu" class="userPopoverMenuWrapper">
<button class="icon-more"
:aria-expanded="openedMenu"
:aria-label="t('settings', 'Toggle user actions menu')"
@click.prevent="toggleMenu" />
<div :class="{ 'open': openedMenu }" class="popovermenu">
<NcPopoverMenu :menu="userActions" />
</div>
</div>
</div>
<div :style="{opacity: feedbackMessage !== '' ? 1 : 0}"
class="feedback">
<div class="icon-checkmark" />
{{ feedbackMessage }}
</div>
<UserRowActions v-if="!loading.all"
:actions="userActions"
:edit="true"
@update:edit="toggleEdit" />
</div>
</div>
</template>
<script>
import ClickOutside from 'vue-click-outside'
import { showSuccess, showError } from '@nextcloud/dialogs'
import NcPopoverMenu from '@nextcloud/vue/dist/Components/NcPopoverMenu.js'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import ClickOutside from 'vue-click-outside'
import UserRowActions from './UserRowActions.vue'
import UserRowSimple from './UserRowSimple.vue'
import UserRowMixin from '../../mixins/UserRowMixin.js'
import { showSuccess, showError } from '@nextcloud/dialogs'
export default {
name: 'UserRow',
components: {
UserRowSimple,
NcPopoverMenu,
NcActions,
NcActionButton,
NcSelect,
NcTextField,
UserRowActions,
UserRowSimple,
},
directives: {
ClickOutside,
},
mixins: [UserRowMixin],
props: {
users: {
type: Array,
@ -325,7 +306,6 @@ export default {
selectedQuota: false,
rand: parseInt(Math.random() * 1000),
openedMenu: false,
feedbackMessage: '',
possibleManagers: [],
currentManager: '',
editing: false,
@ -348,8 +328,8 @@ export default {
editedMail: this.user.email ?? '',
}
},
computed: {
computed: {
/* USER POPOVERMENU ACTIONS */
userActions() {
const actions = [
@ -400,8 +380,10 @@ export default {
return this.languages[0].languages.concat(this.languages[1].languages)
},
},
async beforeMount() {
await this.searchUserManager()
if (this.user.manager) {
await this.initManager(this.user.manager)
}
@ -432,13 +414,14 @@ export default {
this.loading.wipe = true
this.loading.all = true
this.$store.dispatch('wipeUserDevices', userid)
.then(() => {
.then(() => showSuccess(t('settings', 'Wiped {userid}\'s devices', { userid })), { timeout: 2000 })
.finally(() => {
this.loading.wipe = false
this.loading.all = false
})
}
},
true
true,
)
},
@ -500,7 +483,7 @@ export default {
})
}
},
true
true,
)
},
@ -778,19 +761,13 @@ export default {
sendWelcomeMail() {
this.loading.all = true
this.$store.dispatch('sendWelcomeMail', this.user.id)
.then(success => {
if (success) {
// Show feedback to indicate the success
this.feedbackMessage = t('setting', 'Welcome mail sent!')
setTimeout(() => {
this.feedbackMessage = ''
}, 2000)
}
.then(() => showSuccess(t('setting', 'Welcome mail sent!'), { timeout: 2000 }))
.finally(() => {
this.loading.all = false
})
},
handleDoneButton() {
toggleEdit() {
this.editing = false
if (this.editedDisplayName !== this.user.displayname) {
this.editedDisplayName = this.user.displayname
@ -807,7 +784,12 @@ export default {
z-index: 1 !important;
}
.row :deep() {
.row :deep() {
.v-select.select {
// reset min width to 100% instead of X px
min-width: 100%;
}
.mailAddress,
.password,
.displayName {

@ -0,0 +1,78 @@
<template>
<NcActions :aria-label="t('settings', 'Toggle user actions menu')"
:inline="1">
<NcActionButton @click="toggleEdit">
{{ edit ? t('settings', 'Done') : t('settings', 'Edit') }}
<template #icon>
<NcIconSvgWrapper :svg="editSvg" aria-hidden="true" />
</template>
</NcActionButton>
<NcActionButton v-for="(action, index) in actions"
:key="index"
:aria-label="action.text"
:icon="action.icon"
@click="action.action">
{{ action.text }}
</NcActionButton>
</NcActions>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import SvgCheck from '@mdi/svg/svg/check.svg?raw'
import SvgPencil from '@mdi/svg/svg/pencil.svg?raw'
interface UserAction {
action: (event: MouseEvent) => void,
icon: string,
text: string
}
export default defineComponent({
components: {
NcActionButton,
NcActions,
NcIconSvgWrapper,
},
props: {
/**
* Array of user actions
*/
actions: {
type: Array as PropType<readonly UserAction[]>,
required: true,
},
/**
* The state whether the row is currently edited
*/
edit: {
type: Boolean,
required: true,
},
},
computed: {
/**
* Current MDI logo to show for edit toggle
*/
editSvg() {
return this.edit ? SvgCheck : SvgPencil
},
},
methods: {
/**
* Toggle edit mode by emitting the update event
*/
toggleEdit() {
this.$emit('update:edit', !this.edit)
},
},
})
</script>

@ -59,45 +59,26 @@
{{ user.manager }}
</div>
<div class="userActions">
<div v-if="canEdit && !loading.all" class="toggleUserActions">
<NcActions>
<NcActionButton icon="icon-rename"
:title="t('settings', 'Edit User')"
:aria-label="t('settings', 'Edit User')"
@click="toggleEdit" />
</NcActions>
<div class="userPopoverMenuWrapper">
<button v-click-outside="hideMenu"
class="icon-more"
:aria-expanded="openedMenu"
:aria-label="t('settings', 'Toggle user actions menu')"
@click.prevent="toggleMenu" />
<div class="popovermenu" :class="{ 'open': openedMenu }">
<NcPopoverMenu :menu="userActions" />
</div>
</div>
</div>
<div class="feedback" :style="{opacity: feedbackMessage !== '' ? 1 : 0}">
<div class="icon-checkmark" />
{{ feedbackMessage }}
</div>
<UserRowActions v-if="canEdit && !loading.all"
:actions="userActions"
:edit="false"
@update:edit="toggleEdit" />
</div>
</div>
</template>
<script>
import NcPopoverMenu from '@nextcloud/vue/dist/Components/NcPopoverMenu.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import ClickOutside from 'vue-click-outside'
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: {
NcPopoverMenu,
NcActionButton,
NcActions,
UserRowActions,
},
directives: {
ClickOutside,
@ -124,10 +105,6 @@ export default {
type: Boolean,
required: true,
},
feedbackMessage: {
type: String,
required: true,
},
subAdminsGroups: {
type: Array,
required: true,

@ -25,9 +25,8 @@ import { User } from '@nextcloud/cypress'
const admin = new User('admin', 'admin')
const jdoe = new User('jdoe', 'jdoe')
describe('Setting: Users list', function() {
describe('Settings: Create and delete users', function() {
before(function() {
cy.createUser(jdoe)
cy.login(admin)
})
@ -35,48 +34,26 @@ describe('Setting: Users list', function() {
cy.deleteUser(jdoe)
})
it('Can change the password', function() {
it('Can delete a user', function() {
// ensure user exists
cy.createUser(jdoe).login(admin)
// open the User settings
cy.visit('/settings/users')
cy.get(`.user-list-grid .row[data-id="${jdoe.userId}"]`).within(($row) => {
// see that the user is in the list
cy.get(`.user-list-grid .row[data-id="${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('.userActions button .icon-rename').click()
// open the actions menu for the user
cy.get('.userActions button.action-item__menutoggle').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')
// 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()
// 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 password cell for user user0 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 })
// password input is emptied on change
cy.get('input[type="password"]').should('have.value', '')
})
// Success message is shown
cy.get('.toastify.toast-success').contains(/Password.+successfully.+changed/i).should('exist')
// The "Delete user" action in the actions menu is shown and clicked
cy.get('.action-item__popper .action').contains('Delete user').should('exist').click()
// 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')
})
})

@ -0,0 +1,89 @@
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @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/>.
*
*/
import { User } from '@nextcloud/cypress'
const admin = new User('admin', 'admin')
const jdoe = new User('jdoe', 'jdoe')
describe('Settings: Disable and enable users', function() {
before(function() {
cy.createUser(jdoe)
cy.login(admin)
})
after(() => {
cy.deleteUser(jdoe)
})
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(() => {
// 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()
})
// 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')
// 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')
})
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 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()
})
// The "Enable user" action in the actions menu is shown and clicked
cy.get('.action-item__popper .action').contains('Enable user').should('exist').click()
// When clicked the section is not shown anymore
cy.get('#disabled').should('not.exist')
// Make sure it is still gone after the reload reload
cy.reload().login(admin)
cy.get('#disabled').should('not.exist')
})
})

@ -0,0 +1,82 @@
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @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/>.
*
*/
import { User } from '@nextcloud/cypress'
const admin = new User('admin', 'admin')
const jdoe = new User('jdoe', 'jdoe')
describe('Settings: Change user properties', function() {
before(function() {
cy.createUser(jdoe)
cy.login(admin)
})
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) => {
// 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(`.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')
// 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()
// 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 password cell for user user0 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 })
// password input is emptied on change
cy.get('input[type="password"]').should('have.value', '')
})
// Success message is shown
cy.get('.toastify.toast-success').contains(/Password.+successfully.+changed/i).should('exist')
})
})

@ -33,6 +33,11 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable<Subject = any> {
/**
* Enable or disable a given user
*/
enableUser(user: User, enable?: boolean): Cypress.Chainable<Cypress.Response<any>>,
/**
* Upload a file from the fixtures folder to a given user storage.
* **Warning**: Using this function will reset the previous session
@ -69,6 +74,33 @@ declare global {
const url = (Cypress.config('baseUrl') || '').replace(/\/index.php\/?$/g, '')
Cypress.env('baseUrl', url)
/**
* Enable or disable a user
* TODO: standardise in @nextcloud/cypress
*
* @param {User} user the user to dis- / enable
* @param {boolean} enable True if the user should be enable, false to disable
*/
Cypress.Commands.add('enableUser', (user: User, enable = true) => {
const url = `${Cypress.config('baseUrl')}/ocs/v2.php/cloud/users/${user.userId}/${enable ? 'enable' : 'disable'}`.replace('index.php/', '')
return cy.request({
method: 'PUT',
url,
form: true,
auth: {
user: 'admin',
password: 'admin',
},
headers: {
'OCS-ApiRequest': 'true',
'Content-Type': 'application/x-www-form-urlencoded',
},
}).then((response) => {
cy.log(`Enabled user ${user}`, response.status)
return cy.wrap(response)
})
})
/**
* cy.uploadedFile - uploads a file from the fixtures folder
* TODO: standardise in @nextcloud/cypress

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

@ -125,7 +125,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function actionsMenuOf($user) {
return Locator::forThe()->css(".icon-more")->
return Locator::forThe()->css(".userActions .action-item:not(.action-item--single)")->
descendantOf(self::rowForUser($user))->
describedAs("Actions menu for user $user in Users Settings");
}
@ -134,8 +134,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function theAction($action, $user) {
return Locator::forThe()->xpath("//button[normalize-space() = '$action']")->
descendantOf(self::rowForUser($user))->
return Locator::forThe()->xpath("//button[@aria-label = normalize-space('$action')]")->
describedAs("$action action for the user $user row in Users Settings");
}
@ -160,7 +159,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function editModeToggle($user) {
return Locator::forThe()->css(".toggleUserActions button")->
return Locator::forThe()->css(".userActions .action-items button:first-of-type")->
descendantOf(self::rowForUser($user))->
describedAs("The edit toggle button for the user $user in Users Settings");
}

@ -22,42 +22,42 @@ Feature: users
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: 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: 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: 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