Add 'accounts/' from commit 'd2585070bc25d2c8cd0c86476041ee502afb4ac5'

git-subtree-dir: accounts
git-subtree-mainline: 7b8e9bc298
git-subtree-split: d2585070bc
This commit is contained in:
A.Unger
2020-09-18 12:21:51 +02:00
138 changed files with 26531 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
<template>
<div>
<div class="uk-flex uk-flex-column" id="accounts-app">
<template v-if="isInitialized">
<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>
</div>
</template>
<script>
import { mapGetters, mapActions, mapState } from 'vuex'
import AccountsList from './accounts/AccountsList.vue'
import AccountsCreate from './accounts/AccountsCreate.vue'
import AccountsBatchActions from './accounts/AccountsBatchActions.vue'
export default {
name: 'App',
components: { AccountsBatchActions, AccountsList, AccountsCreate },
computed: {
...mapGetters('Accounts', ['isInitialized', 'getAccountsSorted', 'isAnyAccountSelected']),
...mapState('Accounts', ['selectedAccounts']),
accounts () {
return this.getAccountsSorted
},
numberOfSelectedAccounts () {
return this.selectedAccounts.length
}
},
methods: {
...mapActions('Accounts', ['initialize'])
},
created () {
this.initialize()
}
}
</script>
<style>
/* TODO: After https://github.com/owncloud/owncloud-design-system/pull/418 gets merged
there won't be an extra span and this won't be needed anymore */
.accounts-selection-actions-btn > span {
display: flex;
align-items: center;
}
/* TODO: Adjust in ODS */
.oc-dropdown-menu {
width: 150px;
}
</style>
@@ -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>
@@ -0,0 +1,59 @@
<template>
<div>
<oc-table middle divider>
<oc-table-group>
<oc-table-row>
<oc-table-cell shrink type="head">
<oc-checkbox
:value="areAllAccountsSelected"
:label="$gettext('Select all users')"
hide-label
@change="toggleSelectionAll"
/>
</oc-table-cell>
<oc-table-cell shrink type="head" />
<oc-table-cell type="head" v-text="$gettext('Username')" />
<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" v-text="$gettext('Activated')" />
</oc-table-row>
</oc-table-group>
<oc-table-group>
<accounts-list-row
v-for="account in accounts"
:key="`account-list-row-${account.id}`"
:account="account"
/>
</oc-table-group>
</oc-table>
</div>
</template>
<script>
import { mapActions, mapGetters, mapMutations } from 'vuex'
import AccountsListRow from './AccountsListRow.vue'
export default {
name: 'AccountsList',
components: {
AccountsListRow
},
props: {
accounts: {
type: Array,
required: true
}
},
computed: {
...mapGetters('Accounts', ['areAllAccountsSelected'])
},
methods: {
...mapActions('Accounts', ['toggleSelectionAll']),
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION'])
},
beforeDestroy () {
this.RESET_ACCOUNTS_SELECTION()
}
}
</script>
@@ -0,0 +1,161 @@
<template>
<oc-table-row>
<oc-table-cell>
<oc-checkbox :value="isAccountSelected" @change="TOGGLE_SELECTION_ACCOUNT(account)" :label="selectAccountLabel" hide-label />
</oc-table-cell>
<oc-table-cell>
<avatar :user-name="account.displayName || account.onPremisesSamAccountName" :userid="account.id" :width="35" />
</oc-table-cell>
<oc-table-cell v-text="account.onPremisesSamAccountName" />
<oc-table-cell v-text="account.displayName || '-'" />
<oc-table-cell v-text="account.mail" />
<oc-table-cell>
<oc-button :id="`accounts-roles-select-trigger-${account.id}`" class="accounts-roles-select-trigger" variation="raw">
<span class="uk-flex uk-flex-middle accounts-roles-current-role">
{{ currentRole ? currentRole.displayName : $gettext('Select role') }}
<oc-icon name="expand_more" aria-hidden="true" />
</span>
</oc-button>
<oc-drop
:drop-id="`accounts-roles-select-dropdown-${account.id}`"
:toggle="`#accounts-roles-select-trigger-${account.id}`"
mode="click"
close-on-click
:options="{ delayHide: 0 }"
>
<ul class="uk-list">
<li v-for="role in roles" :key="role.id">
<label class="accounts-roles-dropdown-role">
<input
type="radio"
class="oc-radiobutton"
v-model="currentRole"
:value="role"
@change="changeRole(role.id)"
/>
{{ role.displayName }}
</label>
</li>
</ul>
</oc-drop>
</oc-table-cell>
<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 activated')"
class="accounts-status-indicator-enabled"
/>
<oc-icon
v-else
name="deprecated"
key="account-icon-disabled"
variation="danger"
:aria-label="$gettext('Account is blocked')"
class="accounts-status-indicator-disabled"
/>
</oc-table-cell>
</oc-table-row>
</template>
<script>
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'
import { isObjectEmpty } from '../../helpers/utils'
import { injectAuthToken } from '../../helpers/auth'
// eslint-disable-next-line camelcase
import { RoleService_AssignRoleToUser, RoleService_ListRoleAssignments } from '../../client/settings'
import Avatar from './Avatar.vue'
export default {
name: 'AccountsListRow',
components: { Avatar },
props: {
account: {
type: Object,
required: true
}
},
data () {
return {
currentRole: null
}
},
computed: {
...mapGetters(['user', 'configuration']),
...mapState('Accounts', ['roles', 'selectedAccounts']),
isAccountSelected () {
return this.selectedAccounts.indexOf(this.account) > -1
},
selectAccountLabel () {
const translated = this.$gettext('Select %{ account }')
return this.$gettextInterpolate(translated, { account: this.account.displayName }, true)
}
},
created () {
this.getUsersCurrentRole()
},
methods: {
...mapActions(['showMessage']),
...mapMutations('Accounts', ['TOGGLE_SELECTION_ACCOUNT']),
async changeRole (roleId) {
injectAuthToken(this.user.token)
const response = await RoleService_AssignRoleToUser({
$domain: this.configuration.server,
body: {
account_uuid: this.account.id,
role_id: roleId
}
})
if (response.status === 201) {
const roleId = response.data.assignment.roleId
this.currentRole = this.roles.find(role => {
return role.id === roleId
})
} else {
this.showMessage({
title: this.$gettext('Failed to change role.'),
desc: response.statusText,
status: 'danger'
})
}
},
async getUsersCurrentRole () {
injectAuthToken(this.user.token)
const response = await RoleService_ListRoleAssignments({
$domain: this.configuration.server,
body: {
account_uuid: this.account.id
}
})
if (response.status === 201) {
const assignedRole = response.data
if (isObjectEmpty(assignedRole)) {
return
}
this.currentRole = this.roles.find(role => {
return role.id === assignedRole.assignments[0].roleId
})
}
}
}
}
</script>
+130
View File
@@ -0,0 +1,130 @@
<template>
<component :is="type" v-if="enabled">
<oc-spinner
v-if="loading"
key="avatar-loading"
size="small"
:aria-label="$gettext('Loading')"
:style="`width: ${width}px; height: ${width}px;`"
/>
<oc-avatar
v-else
key="avatar-loaded"
:width="width"
:src="avatarSource"
:user-name="userName"
/>
</component>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
/**
* FIXME: this component has been copied over from phoenix. It should be moved over to ODS, then we can reuse it in
* this extension.
*/
name: 'Avatar',
props: {
/**
* The html element used for the avatar container.
* `div, span`
*/
type: {
type: String,
default: 'div',
validator: value => {
return value.match(/(div|span)/)
}
},
userName: {
type: String,
default: ''
},
userid: {
/**
* Allow empty string to show placeholder
*/
type: String,
default: ''
},
width: {
type: Number,
required: false,
default: 42
}
},
data () {
return {
/**
* Set to object URL when loaded, or on failure, icon placeholder is shown
*/
avatarSource: '',
/**
* Shows spinner in place whilst loading avatar from server
*/
loading: true
}
},
computed: {
...mapGetters(['getToken']),
enabled: function () {
return this.$root.config.enableAvatars || true
}
},
watch: {
userid: function (userid, old) {
this.setUser(userid)
}
},
mounted: function () {
// Handled mounted situation. Userid might not be set yet so try placeholder
if (this.userid !== '') {
this.setUser(this.userid)
} else {
this.loading = false
}
},
methods: {
/**
* Load a new avatar from this userid
*/
setUser (userid) {
this.loading = true
this.avatarSource = ''
if (!this.enabled || userid === '') {
this.loading = false
return
}
const headers = new Headers()
const instance = this.$root.config.server || window.location.origin
const url = instance + '/remote.php/dav/avatars/' + this.userid + '/128.png'
headers.append('Authorization', 'Bearer ' + this.getToken)
headers.append('X-Requested-With', 'XMLHttpRequest')
fetch(url, { headers })
.then(response => {
if (response.ok) {
return response.blob()
}
if (response.status !== 404) {
throw new Error(`Unexpected status code ${response.status}`)
}
})
.then(blob => {
this.loading = false
if (blob) {
this.avatarSource = window.URL.createObjectURL(blob)
} else {
// 404, none found
this.avatarSource = ''
}
})
.catch(error => {
this.avatarSource = ''
this.loading = false
console.error(`Error loading avatar image for user "${this.userid}": `, error.message)
})
}
}
}
</script>