mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-28 23:09:46 -05:00
Merge pull request #116 from owncloud/improve-accounts-ui
Improve accounts ui
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
Enhancement: Improve visual appearance of accounts UI
|
||||
|
||||
We aligned the visual appearance of the accounts UI with default ocis-web apps (full width, style of batch actions), added icons to buttons, extracted the buttons from the batch actions dropdown into individual buttons, improved the wording added a confirmation widget for the user deletion and removed the uid and gid columns.
|
||||
|
||||
https://github.com/owncloud/product/issues/222
|
||||
https://github.com/owncloud/ocis-accounts/pull/116
|
||||
@@ -226,6 +226,7 @@ github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/reflex v0.2.0 h1:6d9WpWJseKjJvZEevKP7Pk42nPx2+BUTqmhNk8wZPwM=
|
||||
github.com/cespare/reflex v0.2.0/go.mod h1:ooqOLJ4algvHP/oYvKWfWJ9tFUzCLDk5qkIJduMYrgI=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
@@ -294,6 +295,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/crewjam/httperr v0.0.0-20190612203328-a946449404da h1:WXnT88cFG2davqSFqvaFfzkSMC0lqh/8/rKZ+z7tYvI=
|
||||
github.com/crewjam/httperr v0.0.0-20190612203328-a946449404da/go.mod h1:+rmNIXRvYMqLQeR4DHyTvs6y0MEMymTz4vyFpFkKTPs=
|
||||
@@ -893,6 +895,7 @@ github.com/karrick/godirwalk v1.7.8 h1:VfG72pyIxgtC7+3X9CMHI0AOl4LwyRAg98WAgsvff
|
||||
github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
||||
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
|
||||
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
@@ -916,6 +919,7 @@ github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -1145,6 +1149,7 @@ github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE
|
||||
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliSGTn5uFuqQQJGuNrD0MLCokv09ag=
|
||||
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
github.com/nsqio/go-nsq v1.0.7/go.mod h1:XP5zaUs3pqf+Q71EqUJs3HYfBIqfK6G83WQMdNN+Ito=
|
||||
github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750=
|
||||
github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
|
||||
+3
-32
File diff suppressed because one or more lines are too long
+20
-110
@@ -1,49 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="uk-container uk-padding">
|
||||
<h1 v-text="$gettext('Accounts')" />
|
||||
<oc-button
|
||||
v-if="numberOfSelectedAccounts < 1 && !isAccountCreationInProgress"
|
||||
id="accounts-new-account-trigger"
|
||||
key="create-accounts-button"
|
||||
v-text="$gettext('Create new user')"
|
||||
variation="primary"
|
||||
:disabled="isAccountCreationInProgress || !isInitialized"
|
||||
:uk-tooltip="disabledCreateAccountBtnTooltip"
|
||||
@click="setAccountCreationProgress(true)"
|
||||
/>
|
||||
<accounts-list-new-account-row v-if="isAccountCreationInProgress" @close="setAccountCreationProgress(false)" />
|
||||
<oc-grid v-if="numberOfSelectedAccounts > 0" key="selected-accounts-info" gutter="small" class="uk-flex-middle">
|
||||
<span v-text="selectionInfoText" />
|
||||
<span>|</span>
|
||||
<div>
|
||||
<oc-button v-text="$gettext('Clear selection')" variation="raw" @click="RESET_ACCOUNTS_SELECTION" />
|
||||
</div>
|
||||
<div>
|
||||
<oc-action-drop class="accounts-actions-dropdown">
|
||||
<template v-slot:button>
|
||||
<span class="uk-margin-xsmall-right" v-text="$gettext('Actions')" />
|
||||
<oc-icon name="expand_more" />
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<oc-button
|
||||
v-for="(action, index) in actions"
|
||||
:key="action.label"
|
||||
:id="action.id"
|
||||
variation="raw"
|
||||
role="menuitem"
|
||||
:class="{ 'uk-margin-small-bottom': index + 1 !== actions.length }"
|
||||
class="uk-width-1-1 uk-flex-left"
|
||||
@click="action.handler"
|
||||
>
|
||||
{{ action.label }}
|
||||
</oc-button>
|
||||
</template>
|
||||
</oc-action-drop>
|
||||
</div>
|
||||
</oc-grid>
|
||||
<div class="uk-flex uk-flex-column" id="accounts-app">
|
||||
<template v-if="isInitialized">
|
||||
<accounts-list :accounts="accounts" />
|
||||
<div class="oc-app-bar">
|
||||
<accounts-batch-actions
|
||||
v-if="isAnyAccountSelected"
|
||||
:number-of-selected-accounts="numberOfSelectedAccounts"
|
||||
:selected-accounts="selectedAccounts"
|
||||
/>
|
||||
<accounts-create v-else />
|
||||
</div>
|
||||
<oc-grid class="uk-height-1-1 uk-flex-1 uk-overflow-auto">
|
||||
<div class="uk-width-expand">
|
||||
<accounts-list :accounts="accounts" />
|
||||
</div>
|
||||
</oc-grid>
|
||||
</template>
|
||||
<oc-loader v-else />
|
||||
</div>
|
||||
@@ -51,91 +22,30 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions, mapState, mapMutations } from 'vuex'
|
||||
import { mapGetters, mapActions, mapState } from 'vuex'
|
||||
import AccountsList from './accounts/AccountsList.vue'
|
||||
import AccountsListNewAccountRow from './accounts/AccountsListNewAccountRow.vue'
|
||||
import AccountsCreate from './accounts/AccountsCreate.vue'
|
||||
import AccountsBatchActions from './accounts/AccountsBatchActions.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: { AccountsList, AccountsListNewAccountRow },
|
||||
data: () => ({
|
||||
isAccountCreationInProgress: false
|
||||
}),
|
||||
components: { AccountsBatchActions, AccountsList, AccountsCreate },
|
||||
computed: {
|
||||
...mapGetters('Accounts', ['isInitialized', 'getAccountsSorted']),
|
||||
...mapGetters('Accounts', ['isInitialized', 'getAccountsSorted', 'isAnyAccountSelected']),
|
||||
...mapState('Accounts', ['selectedAccounts']),
|
||||
|
||||
accounts () {
|
||||
return this.getAccountsSorted
|
||||
},
|
||||
|
||||
numberOfSelectedAccounts () {
|
||||
return this.selectedAccounts.length
|
||||
},
|
||||
|
||||
selectionInfoText () {
|
||||
const translated = this.$ngettext('%{ amount } selected user', '%{ amount } selected users', this.numberOfSelectedAccounts)
|
||||
|
||||
return this.$gettextInterpolate(translated, { amount: this.numberOfSelectedAccounts })
|
||||
},
|
||||
|
||||
actions () {
|
||||
const actions = []
|
||||
const numberOfDisabledAccounts = this.selectedAccounts.filter(account => !account.accountEnabled).length
|
||||
const isAnyAccountDisabled = numberOfDisabledAccounts > 0
|
||||
const isAnyAccountEnabled = numberOfDisabledAccounts < this.numberOfSelectedAccounts
|
||||
|
||||
if (isAnyAccountDisabled) {
|
||||
actions.push({
|
||||
id: 'accounts-actions-dropdown-action-enable',
|
||||
label: this.$gettext('Enable'),
|
||||
handler: () => this.toggleAccountStatus(true)
|
||||
})
|
||||
}
|
||||
|
||||
if (isAnyAccountEnabled) {
|
||||
actions.push({
|
||||
id: 'accounts-actions-dropdown-action-disable',
|
||||
label: this.$gettext('Disable'),
|
||||
handler: () => this.toggleAccountStatus(false)
|
||||
})
|
||||
}
|
||||
|
||||
actions.push({
|
||||
id: 'accounts-actions-dropdown-action-delete',
|
||||
label: this.$gettext('Delete'),
|
||||
handler: this.deleteAccounts
|
||||
})
|
||||
|
||||
return actions
|
||||
},
|
||||
|
||||
disabledCreateAccountBtnTooltip () {
|
||||
if (!this.isInitialized) {
|
||||
return this.$gettext('Loading users')
|
||||
}
|
||||
|
||||
if (this.isAccountCreationInProgress) {
|
||||
return this.$gettext('User creation is already in progress')
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('Accounts', ['initialize', 'toggleAccountStatus', 'deleteAccounts']),
|
||||
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION']),
|
||||
|
||||
setAccountCreationProgress (isInProgress) {
|
||||
this.isAccountCreationInProgress = isInProgress
|
||||
}
|
||||
...mapActions('Accounts', ['initialize'])
|
||||
},
|
||||
created () {
|
||||
this.initialize()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.RESET_ACCOUNTS_SELECTION()
|
||||
this.setAccountCreationProgress(false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<oc-grid key="selected-accounts-info" gutter="small" class="uk-flex-middle">
|
||||
<span v-text="selectionInfoText" />
|
||||
<span>|</span>
|
||||
<div>
|
||||
<oc-button v-text="$gettext('Clear selection')" variation="raw" @click="RESET_ACCOUNTS_SELECTION" />
|
||||
</div>
|
||||
<oc-grid gutter="small" id="accounts-batch-actions">
|
||||
<div v-for="action in actions" :key="action.label">
|
||||
<oc-alert v-if="isConfirmationInProgress[action.id]" :variation="action.confirmation.variation || 'default'" noClose class="tmp-alert-fixes">
|
||||
<span>{{ action.confirmation.message }}</span>
|
||||
<oc-button size="small" :id="action.confirmation.cancel.id" @click="action.confirmation.cancel.handler" :variation="action.confirmation.cancel.variation || 'default'">
|
||||
{{ action.confirmation.cancel.label }}
|
||||
</oc-button>
|
||||
<oc-button size="small" :id="action.confirmation.confirm.id" @click="action.confirmation.confirm.handler" :variation="action.confirmation.confirm.variation || 'default'">
|
||||
{{ action.confirmation.confirm.label }}
|
||||
</oc-button>
|
||||
</oc-alert>
|
||||
<oc-button v-else :id="action.id" @click="action.handler" :variation="action.variation || 'default'" :icon="action.icon">
|
||||
{{ action.label }}
|
||||
</oc-button>
|
||||
</div>
|
||||
</oc-grid>
|
||||
</oc-grid>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'AccountsBatchActions',
|
||||
props: {
|
||||
numberOfSelectedAccounts: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
selectedAccounts: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
isConfirmationInProgress: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectionInfoText () {
|
||||
const translated = this.$ngettext('%{ amount } selected user', '%{ amount } selected users', this.numberOfSelectedAccounts)
|
||||
return this.$gettextInterpolate(translated, { amount: this.numberOfSelectedAccounts })
|
||||
},
|
||||
actions () {
|
||||
const actions = []
|
||||
const numberOfDisabledAccounts = this.selectedAccounts.filter(account => !account.accountEnabled).length
|
||||
const isAnyAccountDisabled = numberOfDisabledAccounts > 0
|
||||
const isAnyAccountEnabled = numberOfDisabledAccounts < this.numberOfSelectedAccounts
|
||||
|
||||
if (isAnyAccountDisabled) {
|
||||
actions.push({
|
||||
id: 'accounts-batch-action-enable',
|
||||
label: this.$gettext('Activate'),
|
||||
icon: 'ready',
|
||||
handler: () => this.setAccountActivated(true)
|
||||
})
|
||||
}
|
||||
|
||||
if (isAnyAccountEnabled) {
|
||||
actions.push({
|
||||
id: 'accounts-batch-action-disable',
|
||||
label: this.$gettext('Block'),
|
||||
icon: 'deprecated',
|
||||
handler: () => this.setAccountActivated(false)
|
||||
})
|
||||
}
|
||||
|
||||
const idDeleteAction = 'accounts-batch-action-delete'
|
||||
actions.push({
|
||||
id: idDeleteAction,
|
||||
label: this.$gettext('Delete'),
|
||||
icon: 'delete',
|
||||
handler: () => this.showConfirmationRequest(idDeleteAction),
|
||||
confirmation: {
|
||||
variation: 'danger',
|
||||
message: this.$ngettext(
|
||||
'Delete the selected account?',
|
||||
'Delete the selected accounts?',
|
||||
this.numberOfSelectedAccounts
|
||||
),
|
||||
cancel: {
|
||||
id: 'accounts-batch-action-delete-cancel',
|
||||
label: this.$gettext('Cancel'),
|
||||
variation: 'secondary',
|
||||
handler: () => this.hideConfirmationRequest(idDeleteAction)
|
||||
},
|
||||
confirm: {
|
||||
id: 'accounts-batch-action-delete-confirm',
|
||||
label: this.$gettext('Confirm'),
|
||||
variation: 'danger',
|
||||
handler: this.deleteAccounts
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return actions
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('Accounts', ['setAccountActivated', 'deleteAccounts']),
|
||||
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION']),
|
||||
showConfirmationRequest (actionId) {
|
||||
this.isConfirmationInProgress = { ...this.isConfirmationInProgress, [actionId]: true }
|
||||
},
|
||||
hideConfirmationRequest (actionId) {
|
||||
this.isConfirmationInProgress = { ...this.isConfirmationInProgress, [actionId]: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tmp-alert-fixes {
|
||||
padding: 5px 10px 4px !important;
|
||||
border-radius: 3px !important;
|
||||
background-color: #fff !important;
|
||||
border: 1px solid rgb(224, 0, 0) !important;
|
||||
color: rgb(224, 0, 0) !important;
|
||||
|
||||
font-size: 1.125rem !important;
|
||||
font-weight: 600 !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div>
|
||||
<oc-grid v-if="isFormInProgress" gutter="small">
|
||||
<label>
|
||||
<oc-text-input
|
||||
id="accounts-new-account-input-username"
|
||||
type="text"
|
||||
v-model="formData.username"
|
||||
:error-message="formValidation.usernameError"
|
||||
:placeholder="$gettext('Username')"
|
||||
:disabled="isRequestInProgress"
|
||||
@keydown.enter="createAccount"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<oc-text-input
|
||||
id="accounts-new-account-input-email"
|
||||
type="email"
|
||||
v-model="formData.email"
|
||||
:error-message="formValidation.emailError"
|
||||
:placeholder="$gettext('Email')"
|
||||
:disabled="isRequestInProgress"
|
||||
@keydown.enter="createAccount"
|
||||
/>
|
||||
</label>
|
||||
<label class="uk-margin-xsmall-right">
|
||||
<oc-text-input
|
||||
id="accounts-new-account-input-password"
|
||||
type="password"
|
||||
v-model="formData.password"
|
||||
:error-message="formValidation.passwordError"
|
||||
:placeholder="$gettext('Password')"
|
||||
:disabled="isRequestInProgress"
|
||||
@keydown.enter="createAccount"
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<oc-button
|
||||
v-text="$gettext('Cancel')"
|
||||
@click="cancelForm"
|
||||
class="uk-margin-xsmall-right"
|
||||
:disabled="isRequestInProgress"
|
||||
/>
|
||||
<oc-button
|
||||
id="accounts-new-account-button-confirm"
|
||||
variation="primary"
|
||||
:disabled="isRequestInProgress"
|
||||
@click="createAccount"
|
||||
>
|
||||
<oc-spinner
|
||||
v-if="isRequestInProgress"
|
||||
key="account-creation-in-progress"
|
||||
size="xsmall"
|
||||
class="uk-margin-xsmall-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-text="isRequestInProgress ? $gettext('Creating') : $gettext('Create')" />
|
||||
</oc-button>
|
||||
</div>
|
||||
</oc-grid>
|
||||
<oc-grid v-else gutter="small">
|
||||
<div>
|
||||
<oc-button
|
||||
id="accounts-new-account-trigger"
|
||||
key="create-accounts-button"
|
||||
icon="add"
|
||||
variation="primary"
|
||||
@click="setFormInProgress(true)"
|
||||
>
|
||||
<translate>Create new account</translate>
|
||||
</oc-button>
|
||||
</div>
|
||||
</oc-grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import isEmail from 'validator/es/lib/isEmail'
|
||||
import isEmpty from 'validator/es/lib/isEmpty'
|
||||
import debounce from 'debounce'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'AccountsCreate',
|
||||
|
||||
data: () => ({
|
||||
isFormInProgress: false,
|
||||
isRequestInProgress: false,
|
||||
formData: {
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
formValidation: {
|
||||
usernameError: '',
|
||||
emailError: '',
|
||||
passwordError: ''
|
||||
}
|
||||
}),
|
||||
|
||||
methods: {
|
||||
...mapActions('Accounts', ['createNewAccount']),
|
||||
|
||||
setFormInProgress (inProgress) {
|
||||
this.isFormInProgress = inProgress
|
||||
},
|
||||
|
||||
cancelForm () {
|
||||
this.isRequestInProgress = false
|
||||
this.setFormInProgress(false)
|
||||
this.formData = {
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
}
|
||||
this.formValidation = {
|
||||
usernameError: '',
|
||||
emailError: '',
|
||||
passwordError: ''
|
||||
}
|
||||
},
|
||||
|
||||
createAccount () {
|
||||
// note: use bitwise AND because we want all checks to be performed
|
||||
if (!(this.checkUsername() & this.checkEmail() & this.checkPassword())) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isRequestInProgress = true
|
||||
this.createNewAccount(this.formData)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
this.cancelForm()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.isRequestInProgress = false
|
||||
})
|
||||
},
|
||||
|
||||
checkUsername () {
|
||||
if (isEmpty(this.formData.username)) {
|
||||
debounce(this.formValidation.usernameError = this.$gettext('Username cannot be empty'), 500)
|
||||
return false
|
||||
}
|
||||
// hacky check: we want to allow emails and the username part of emails as username
|
||||
if (!isEmail(this.formData.username) && !isEmail(this.formData.username + '@validate.it')) {
|
||||
debounce(this.formValidation.usernameError = this.$gettext('Invalid username'), 500)
|
||||
return false
|
||||
}
|
||||
|
||||
this.formValidation.usernameError = ''
|
||||
return true
|
||||
},
|
||||
|
||||
checkEmail () {
|
||||
if (isEmpty(this.formData.email)) {
|
||||
debounce(this.formValidation.emailError = this.$gettext('Email cannot be empty'), 500)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isEmail(this.formData.email)) {
|
||||
debounce(this.formValidation.emailError = this.$gettext('Invalid email address'), 500)
|
||||
return false
|
||||
}
|
||||
|
||||
this.formValidation.emailError = ''
|
||||
return true
|
||||
},
|
||||
|
||||
checkPassword () {
|
||||
// Later on some restrictions might be applied here
|
||||
if (isEmpty(this.formData.password)) {
|
||||
debounce(this.formValidation.passwordError = this.$gettext('Password cannot be empty'), 500)
|
||||
return false
|
||||
}
|
||||
|
||||
this.formValidation.passwordError = ''
|
||||
return true
|
||||
}
|
||||
},
|
||||
onDestroy () {
|
||||
this.cancelForm()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#accounts-new-account-button-confirm > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -16,9 +16,7 @@
|
||||
<oc-table-cell type="head" v-text="$gettext('Display name')" />
|
||||
<oc-table-cell type="head" v-text="$gettext('Email')" />
|
||||
<oc-table-cell type="head" v-text="$gettext('Role')" />
|
||||
<oc-table-cell shrink type="head" class="uk-text-nowrap" v-text="$gettext('Uid number')" />
|
||||
<oc-table-cell shrink type="head" class="uk-text-nowrap" v-text="$gettext('Gid number')" />
|
||||
<oc-table-cell shrink type="head" v-text="$gettext('Enabled')" />
|
||||
<oc-table-cell shrink type="head" v-text="$gettext('Activated')" />
|
||||
</oc-table-row>
|
||||
</oc-table-group>
|
||||
<oc-table-group>
|
||||
@@ -33,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { mapActions, mapGetters, mapMutations } from 'vuex'
|
||||
import AccountsListRow from './AccountsListRow.vue'
|
||||
|
||||
export default {
|
||||
@@ -51,7 +49,11 @@ export default {
|
||||
...mapGetters('Accounts', ['areAllAccountsSelected'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions('Accounts', ['toggleSelectionAll'])
|
||||
...mapActions('Accounts', ['toggleSelectionAll']),
|
||||
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION'])
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.RESET_ACCOUNTS_SELECTION()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<div class="uk-flex uk-flex-top">
|
||||
<oc-grid gutter="small">
|
||||
<label>
|
||||
<oc-text-input
|
||||
id="accounts-new-account-input-username"
|
||||
type="text"
|
||||
v-model="username"
|
||||
:error-message="usernameError"
|
||||
:placeholder="$gettext('Username')"
|
||||
:disabled="isInProgress"
|
||||
@keydown.enter="createAccount"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<oc-text-input
|
||||
id="accounts-new-account-input-email"
|
||||
type="email"
|
||||
v-model="email"
|
||||
:error-message="emailError"
|
||||
:placeholder="$gettext('Email')"
|
||||
:disabled="isInProgress"
|
||||
@keydown.enter="createAccount"
|
||||
/>
|
||||
</label>
|
||||
<label class="uk-margin-xsmall-right">
|
||||
<oc-text-input
|
||||
id="accounts-new-account-input-password"
|
||||
type="password"
|
||||
v-model="password"
|
||||
:error-message="passwordError"
|
||||
:placeholder="$gettext('Password')"
|
||||
:disabled="isInProgress"
|
||||
@keydown.enter="createAccount"
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<oc-button
|
||||
v-text="$gettext('Cancel')"
|
||||
@click="emitClose"
|
||||
class="uk-margin-xsmall-right"
|
||||
:disabled="isInProgress"
|
||||
/>
|
||||
<oc-button
|
||||
id="accounts-new-account-button-confirm"
|
||||
variation="primary"
|
||||
:disabled="isInProgress"
|
||||
@click="createAccount"
|
||||
>
|
||||
<oc-spinner
|
||||
v-if="isInProgress"
|
||||
key="account-creation-in-progress"
|
||||
size="xsmall"
|
||||
class="uk-margin-xsmall-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-text="isInProgress ? $gettext('Creating') : $gettext('Create')" />
|
||||
</oc-button>
|
||||
</div>
|
||||
</oc-grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import isEmail from 'validator/es/lib/isEmail'
|
||||
import isEmpty from 'validator/es/lib/isEmpty'
|
||||
import debounce from 'debounce'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'AccountsListNewAccountRow',
|
||||
|
||||
data: () => ({
|
||||
username: '',
|
||||
usernameError: '',
|
||||
email: '',
|
||||
emailError: '',
|
||||
password: '',
|
||||
passwordError: '',
|
||||
isInProgress: false
|
||||
}),
|
||||
|
||||
methods: {
|
||||
...mapActions('Accounts', ['createNewAccount']),
|
||||
|
||||
emitClose () {
|
||||
this.$emit('close')
|
||||
},
|
||||
createAccount () {
|
||||
if (!(this.checkUsername() & this.checkEmail() & this.checkPassword())) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isInProgress = true
|
||||
this.createNewAccount({ username: this.username, email: this.email, password: this.password }).finally(() => {
|
||||
this.isInProgress = false
|
||||
})
|
||||
this.emitClose()
|
||||
},
|
||||
|
||||
checkUsername () {
|
||||
if (isEmpty(this.username)) {
|
||||
debounce(this.usernameError = this.$gettext('Username cannot be empty'), 500)
|
||||
return false
|
||||
}
|
||||
|
||||
this.usernameError = ''
|
||||
return true
|
||||
},
|
||||
|
||||
checkEmail () {
|
||||
if (isEmpty(this.email)) {
|
||||
debounce(this.emailError = this.$gettext('Email cannot be empty'), 500)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isEmail(this.email)) {
|
||||
debounce(this.emailError = this.$gettext('Invalid email address'), 500)
|
||||
return false
|
||||
}
|
||||
|
||||
this.emailError = ''
|
||||
return true
|
||||
},
|
||||
|
||||
checkPassword () {
|
||||
// Later on some restrictions might be applied here
|
||||
if (isEmpty(this.password)) {
|
||||
debounce(this.passwordError = this.$gettext('Password cannot be empty'), 500)
|
||||
return false
|
||||
}
|
||||
|
||||
this.passwordError = ''
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#accounts-new-account-button-confirm > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -39,15 +39,13 @@
|
||||
</ul>
|
||||
</oc-drop>
|
||||
</oc-table-cell>
|
||||
<oc-table-cell v-text="account.uidNumber || '-'" />
|
||||
<oc-table-cell v-text="account.gidNumber || '-'" />
|
||||
<oc-table-cell class="uk-text-center">
|
||||
<oc-icon
|
||||
v-if="account.accountEnabled"
|
||||
key="account-icon-enabled"
|
||||
name="ready"
|
||||
variation="success"
|
||||
:aria-label="$gettext('Account is enabled')"
|
||||
:aria-label="$gettext('Account is activated')"
|
||||
class="accounts-status-indicator-enabled"
|
||||
/>
|
||||
<oc-icon
|
||||
@@ -55,7 +53,7 @@
|
||||
name="deprecated"
|
||||
key="account-icon-disabled"
|
||||
variation="danger"
|
||||
:aria-label="$gettext('Account is disabled')"
|
||||
:aria-label="$gettext('Account is blocked')"
|
||||
class="accounts-status-indicator-disabled"
|
||||
/>
|
||||
</oc-table-cell>
|
||||
|
||||
+72
-74
@@ -28,7 +28,8 @@ const getters = {
|
||||
return a1.onPremisesSamAccountName.localeCompare(a2.onPremisesSamAccountName)
|
||||
})
|
||||
},
|
||||
areAllAccountsSelected: state => state.accounts.length === state.selectedAccounts.length
|
||||
areAllAccountsSelected: state => state.accounts.length === state.selectedAccounts.length,
|
||||
isAnyAccountSelected: state => state.selectedAccounts.length > 0
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
@@ -128,86 +129,86 @@ const actions = {
|
||||
getters.areAllAccountsSelected ? commit('RESET_ACCOUNTS_SELECTION') : commit('SET_SELECTED_ACCOUNTS', [...state.accounts])
|
||||
},
|
||||
|
||||
async toggleAccountStatus ({ commit, dispatch, state, rootGetters }, status) {
|
||||
async setAccountActivated ({ commit, dispatch, state, rootGetters }, activated) {
|
||||
const failedAccounts = []
|
||||
injectAuthToken(rootGetters.user.token)
|
||||
|
||||
for (const account of state.selectedAccounts) {
|
||||
if (account.accountEnabled === status) {
|
||||
if (account.accountEnabled === activated) {
|
||||
continue
|
||||
}
|
||||
|
||||
const response = await AccountsService_UpdateAccount({
|
||||
$domain: rootGetters.configuration.server,
|
||||
body: {
|
||||
account: {
|
||||
id: account.id,
|
||||
accountEnabled: status
|
||||
},
|
||||
update_mask: {
|
||||
paths: ['AccountEnabled']
|
||||
try {
|
||||
const response = await AccountsService_UpdateAccount({
|
||||
$domain: rootGetters.configuration.server,
|
||||
body: {
|
||||
account: {
|
||||
id: account.id,
|
||||
accountEnabled: activated
|
||||
},
|
||||
update_mask: {
|
||||
paths: ['AccountEnabled']
|
||||
}
|
||||
}
|
||||
})
|
||||
if (response.status === 201) {
|
||||
commit('UPDATE_ACCOUNT', { ...account, accountEnabled: activated })
|
||||
} else {
|
||||
failedAccounts.push({ account: account.username })
|
||||
}
|
||||
})
|
||||
|
||||
if (response.status === 201) {
|
||||
commit('UPDATE_ACCOUNT', { ...account, accountEnabled: status })
|
||||
} else {
|
||||
failedAccounts.push({ account: account.diisplayName, statusText: response.statusText })
|
||||
} catch (error) {
|
||||
failedAccounts.push({ account: account.username })
|
||||
}
|
||||
}
|
||||
|
||||
if (failedAccounts.length === 1) {
|
||||
const failedMessageTitle = status ? 'Failed to enable account.' : 'Failed to disable account.'
|
||||
|
||||
if (failedAccounts.length > 0) {
|
||||
let errorTitle = ''
|
||||
if (failedAccounts.length === 1) {
|
||||
errorTitle = activated ? 'Failed to activate account.' : 'Failed to block account.'
|
||||
} else {
|
||||
errorTitle = activated ? 'Failed to activate accounts.' : 'Failed to block accounts.'
|
||||
}
|
||||
dispatch('showMessage', {
|
||||
title: failedMessageTitle,
|
||||
desc: failedAccounts[0].statusText,
|
||||
status: 'danger'
|
||||
}, { root: true })
|
||||
}
|
||||
|
||||
if (failedAccounts.length > 1) {
|
||||
const failedMessageTitle = status ? 'Failed to enable accounts.' : 'Failed to disable accounts.'
|
||||
const failedMessageDesc = status ? 'Could not enable multiple accounts.' : 'Could not disable multiple accounts.'
|
||||
|
||||
dispatch('showMessage', {
|
||||
title: failedMessageTitle,
|
||||
desc: failedMessageDesc,
|
||||
title: errorTitle,
|
||||
status: 'danger'
|
||||
}, { root: true })
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
commit('RESET_ACCOUNTS_SELECTION')
|
||||
return Promise.resolve(true)
|
||||
},
|
||||
async createNewAccount ({ rootGetters, commit, dispatch }, account) {
|
||||
injectAuthToken(rootGetters.user.token)
|
||||
|
||||
const response = await AccountsService_CreateAccount({
|
||||
$domain: rootGetters.configuration.server,
|
||||
body: {
|
||||
account: {
|
||||
on_premises_sam_account_name: account.username,
|
||||
preferred_name: account.username,
|
||||
mail: account.email,
|
||||
password_profile: {
|
||||
password: account.password
|
||||
},
|
||||
account_enabled: true,
|
||||
display_name: account.username
|
||||
try {
|
||||
const response = await AccountsService_CreateAccount({
|
||||
$domain: rootGetters.configuration.server,
|
||||
body: {
|
||||
account: {
|
||||
on_premises_sam_account_name: account.username,
|
||||
preferred_name: account.username,
|
||||
mail: account.email,
|
||||
password_profile: {
|
||||
password: account.password
|
||||
},
|
||||
account_enabled: true,
|
||||
display_name: account.username
|
||||
}
|
||||
}
|
||||
})
|
||||
if (response.status === 201) {
|
||||
commit('PUSH_NEW_ACCOUNT', response.data)
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
})
|
||||
|
||||
if (response.status === 201) {
|
||||
commit('PUSH_NEW_ACCOUNT', response.data)
|
||||
} else {
|
||||
} catch (error) {
|
||||
dispatch('showMessage', {
|
||||
title: 'Failed to create account',
|
||||
desc: response.statusText,
|
||||
title: 'Failed to create account.',
|
||||
status: 'danger'
|
||||
}, { root: true })
|
||||
return Promise.reject(error)
|
||||
}
|
||||
return Promise.resolve(false)
|
||||
},
|
||||
|
||||
async deleteAccounts ({ rootGetters, state, commit, dispatch }) {
|
||||
@@ -216,37 +217,34 @@ const actions = {
|
||||
injectAuthToken(rootGetters.user.token)
|
||||
|
||||
for (const account of state.selectedAccounts) {
|
||||
const response = await AccountsService_DeleteAccount({
|
||||
$domain: rootGetters.configuration.server,
|
||||
body: {
|
||||
id: account.id
|
||||
try {
|
||||
const response = await AccountsService_DeleteAccount({
|
||||
$domain: rootGetters.configuration.server,
|
||||
body: {
|
||||
id: account.id
|
||||
}
|
||||
})
|
||||
if (response.status === 201 || response.status === 204) {
|
||||
commit('DELETE_ACCOUNT', account.id)
|
||||
} else {
|
||||
failedAccounts.push({ account: account.username })
|
||||
}
|
||||
})
|
||||
|
||||
if (response.status === 201 || response.status === 204) {
|
||||
commit('DELETE_ACCOUNT', account.id)
|
||||
} else {
|
||||
failedAccounts.push({ account: account.diisplayName, statusText: response.statusText })
|
||||
} catch (error) {
|
||||
failedAccounts.push({ account: account.username })
|
||||
}
|
||||
}
|
||||
|
||||
if (failedAccounts.length === 1) {
|
||||
if (failedAccounts.length > 0) {
|
||||
const errorTitle = failedAccounts.length === 1 ? 'Failed to delete account.' : 'Failed to delete accounts.'
|
||||
dispatch('showMessage', {
|
||||
title: 'Failed to delete account',
|
||||
desc: failedAccounts[0].statusText,
|
||||
status: 'danger'
|
||||
}, { root: true })
|
||||
}
|
||||
|
||||
if (failedAccounts.length > 1) {
|
||||
dispatch('showMessage', {
|
||||
title: 'Failed to delete accounts',
|
||||
desc: 'Could not delete multiple accounts',
|
||||
title: errorTitle,
|
||||
status: 'danger'
|
||||
}, { root: true })
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
commit('RESET_ACCOUNTS_SELECTION')
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
commands: {
|
||||
navigateAndWaitTillLoaded: async function () {
|
||||
navigateAndWaitUntilMounted: async function () {
|
||||
const url = this.url()
|
||||
return this.navigate(url).waitForElementVisible('@accountsLabel')
|
||||
return this.navigate(url).waitForElementVisible('@accountsApp')
|
||||
},
|
||||
accountsList: function () {
|
||||
return this.waitForElementVisible('@accountsListTable')
|
||||
@@ -43,18 +43,9 @@ module.exports = {
|
||||
return this.useXpath().expect.element(roleSelector).to.be.visible
|
||||
},
|
||||
|
||||
toggleUserStatus: function (usernames, status) {
|
||||
const actionSelector = status === 'enabled' ? this.elements.enableAction : this.elements.disableAction
|
||||
|
||||
setUserActivated: function (usernames, activated) {
|
||||
this.selectUsers(usernames)
|
||||
|
||||
return this
|
||||
.waitForElementVisible('@actionsDropdownTrigger')
|
||||
.click('@actionsDropdownTrigger')
|
||||
.useCss()
|
||||
.waitForElementVisible(actionSelector)
|
||||
.click(actionSelector)
|
||||
.useXpath()
|
||||
return this.click(activated === true ? this.elements.batchActionEnable : this.elements.batchActionDisable)
|
||||
},
|
||||
|
||||
checkUsersStatus: function (usernames, status) {
|
||||
@@ -73,11 +64,9 @@ module.exports = {
|
||||
|
||||
deleteUsers: function (usernames) {
|
||||
this.selectUsers(usernames)
|
||||
|
||||
return this
|
||||
.waitForElementVisible('@actionsDropdownTrigger')
|
||||
.click('@actionsDropdownTrigger')
|
||||
.click('@deleteAction')
|
||||
return this.click(this.elements.batchActionDelete)
|
||||
.waitForElementVisible(this.elements.batchActionDeleteConfirm)
|
||||
.click(this.elements.batchActionDeleteConfirm)
|
||||
},
|
||||
|
||||
selectUsers: function (usernames) {
|
||||
@@ -105,9 +94,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
elements: {
|
||||
accountsLabel: {
|
||||
selector: "//h1[normalize-space(.)='Accounts']",
|
||||
locateStrategy: 'xpath'
|
||||
accountsApp: {
|
||||
selector: '#accounts-app'
|
||||
},
|
||||
accountsListTable: {
|
||||
selector: "//table[@class='uk-table uk-table-middle uk-table-divider']",
|
||||
@@ -141,15 +129,20 @@ module.exports = {
|
||||
selector: '//input[@class="oc-checkbox"]',
|
||||
locateStrategy: 'xpath'
|
||||
},
|
||||
actionsDropdownTrigger: {
|
||||
selector: '//div[contains(@class, "accounts-actions-dropdown")]//button[normalize-space()="Actions"]',
|
||||
locateStrategy: 'xpath'
|
||||
batchActionDisable: {
|
||||
selector: '#accounts-batch-action-disable'
|
||||
},
|
||||
disableAction: {
|
||||
selector: '#accounts-actions-dropdown-action-disable'
|
||||
batchActionEnable: {
|
||||
selector: '#accounts-batch-action-enable'
|
||||
},
|
||||
enableAction: {
|
||||
selector: '#accounts-actions-dropdown-action-enable'
|
||||
batchActionDelete: {
|
||||
selector: '#accounts-batch-action-delete'
|
||||
},
|
||||
batchActionDeleteCancel: {
|
||||
selector: '#accounts-batch-action-delete-cancel'
|
||||
},
|
||||
batchActionDeleteConfirm: {
|
||||
selector: '#accounts-batch-action-delete-confirm'
|
||||
},
|
||||
statusIndicator: {
|
||||
selector: '//span[contains(@class, "accounts-status-indicator-%s")]',
|
||||
@@ -169,9 +162,6 @@ module.exports = {
|
||||
},
|
||||
accountsNewAccountTrigger: {
|
||||
selector: '#accounts-new-account-trigger'
|
||||
},
|
||||
deleteAction: {
|
||||
selector: '#accounts-actions-dropdown-action-delete'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const { client } = require('nightwatch-api')
|
||||
const { Given, When, Then } = require('cucumber')
|
||||
|
||||
When('the user browses to the accounts page', function () {
|
||||
return client.page.accountsPage().navigateAndWaitTillLoaded()
|
||||
return client.page.accountsPage().navigateAndWaitUntilMounted()
|
||||
})
|
||||
|
||||
Then('user {string} should be displayed in the accounts list on the WebUI', async function (username) {
|
||||
@@ -38,11 +38,11 @@ Then('the user should not be able to see the accounts list on the WebUI', async
|
||||
})
|
||||
|
||||
When('the user disables user/users {string} using the WebUI', function (usernames) {
|
||||
return client.page.accountsPage().toggleUserStatus(usernames, 'disabled')
|
||||
return client.page.accountsPage().setUserActivated(usernames, false)
|
||||
})
|
||||
|
||||
When('the user enables user/users {string} using the WebUI', function (usernames) {
|
||||
return client.page.accountsPage().toggleUserStatus(usernames, 'enabled')
|
||||
return client.page.accountsPage().setUserActivated(usernames, true)
|
||||
})
|
||||
|
||||
Then('the status indicator of user/users {string} should be {string} on the WebUI', function (usernames, status) {
|
||||
|
||||
Reference in New Issue
Block a user