parent
97a93c73ce
commit
cbfe0c67e9
File diff suppressed because one or more lines are too long
@ -0,0 +1,126 @@ |
||||
<!-- |
||||
- @copyright 2023 Christopher Ng <chrng8@gmail.com> |
||||
- |
||||
- @author Christopher Ng <chrng8@gmail.com> |
||||
- |
||||
- @license AGPL-3.0-or-later |
||||
- |
||||
- This program is free software: you can redistribute it and/or modify |
||||
- it under the terms of the GNU Affero General Public License as |
||||
- published by the Free Software Foundation, either version 3 of the |
||||
- License, or (at your option) any later version. |
||||
- |
||||
- This program is distributed in the hope that it will be useful, |
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
- GNU Affero General Public License for more details. |
||||
- |
||||
- You should have received a copy of the GNU Affero General Public License |
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
- |
||||
--> |
||||
|
||||
<template> |
||||
<tr class="footer"> |
||||
<th scope="row"> |
||||
<span class="hidden-visually">{{ t('settings', 'Total rows summary') }}</span> |
||||
</th> |
||||
<td class="footer__cell footer__cell--loading"> |
||||
<NcLoadingIcon v-if="loading" |
||||
:title="t('settings', 'Loading users …')" |
||||
:size="32" /> |
||||
</td> |
||||
<td class="footer__cell footer__cell--count footer__cell--multiline"> |
||||
<span aria-describedby="user-count-desc">{{ userCount }}</span> |
||||
<span id="user-count-desc" |
||||
class="hidden-visually"> |
||||
{{ t('settings', 'Scroll to load more rows') }} |
||||
</span> |
||||
</td> |
||||
</tr> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import Vue from 'vue' |
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' |
||||
|
||||
import { |
||||
translate as t, |
||||
translatePlural as n, |
||||
} from '@nextcloud/l10n' |
||||
|
||||
export default Vue.extend({ |
||||
name: 'UserListFooter', |
||||
|
||||
components: { |
||||
NcLoadingIcon, |
||||
}, |
||||
|
||||
props: { |
||||
loading: { |
||||
type: Boolean, |
||||
required: true, |
||||
}, |
||||
filteredUsers: { |
||||
type: Array, |
||||
required: true, |
||||
}, |
||||
}, |
||||
|
||||
computed: { |
||||
userCount(): string { |
||||
if (this.loading) { |
||||
return this.n( |
||||
'settings', |
||||
'{userCount} user …', |
||||
'{userCount} users …', |
||||
this.filteredUsers.length, |
||||
{ |
||||
userCount: this.filteredUsers.length, |
||||
}, |
||||
) |
||||
} |
||||
return this.n( |
||||
'settings', |
||||
'{userCount} user', |
||||
'{userCount} users', |
||||
this.filteredUsers.length, |
||||
{ |
||||
userCount: this.filteredUsers.length, |
||||
}, |
||||
) |
||||
}, |
||||
}, |
||||
|
||||
methods: { |
||||
t, |
||||
n, |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
@import './shared/styles.scss'; |
||||
|
||||
.footer { |
||||
@include row; |
||||
@include cell; |
||||
|
||||
&__cell { |
||||
position: sticky; |
||||
color: var(--color-text-maxcontrast); |
||||
|
||||
&--loading { |
||||
left: 0; |
||||
width: var(--avatar-cell-width); |
||||
align-items: center; |
||||
padding: 0; |
||||
} |
||||
|
||||
&--count { |
||||
left: var(--avatar-cell-width); |
||||
width: var(--cell-width); |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,150 @@ |
||||
<!-- |
||||
- @copyright 2023 Christopher Ng <chrng8@gmail.com> |
||||
- |
||||
- @author Christopher Ng <chrng8@gmail.com> |
||||
- |
||||
- @license AGPL-3.0-or-later |
||||
- |
||||
- This program is free software: you can redistribute it and/or modify |
||||
- it under the terms of the GNU Affero General Public License as |
||||
- published by the Free Software Foundation, either version 3 of the |
||||
- License, or (at your option) any later version. |
||||
- |
||||
- This program is distributed in the hope that it will be useful, |
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
- GNU Affero General Public License for more details. |
||||
- |
||||
- You should have received a copy of the GNU Affero General Public License |
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
- |
||||
--> |
||||
|
||||
<template> |
||||
<tr class="header"> |
||||
<th class="header__cell header__cell--avatar" |
||||
scope="col"> |
||||
<span class="hidden-visually"> |
||||
{{ t('settings', 'Avatar') }} |
||||
</span> |
||||
</th> |
||||
<th class="header__cell header__cell--displayname" |
||||
scope="col"> |
||||
<strong> |
||||
{{ t('settings', 'Display name') }} |
||||
</strong> |
||||
<span class="header__subtitle"> |
||||
{{ t('settings', 'Username') }} |
||||
</span> |
||||
</th> |
||||
<th class="header__cell" |
||||
:class="{ 'header__cell--obfuscated': hasObfuscated }" |
||||
scope="col"> |
||||
<span>{{ passwordLabel }}</span> |
||||
</th> |
||||
<th class="header__cell" |
||||
scope="col"> |
||||
<span>{{ t('settings', 'Email') }}</span> |
||||
</th> |
||||
<th class="header__cell header__cell--large" |
||||
scope="col"> |
||||
<span>{{ t('settings', 'Groups') }}</span> |
||||
</th> |
||||
<th v-if="subAdminsGroups.length > 0 && settings.isAdmin" |
||||
class="header__cell header__cell--large" |
||||
scope="col"> |
||||
<span>{{ t('settings', 'Group admin for') }}</span> |
||||
</th> |
||||
<th class="header__cell" |
||||
scope="col"> |
||||
<span>{{ t('settings', 'Quota') }}</span> |
||||
</th> |
||||
<th v-if="showConfig.showLanguages" |
||||
class="header__cell header__cell--large" |
||||
scope="col"> |
||||
<span>{{ t('settings', 'Language') }}</span> |
||||
</th> |
||||
<th v-if="showConfig.showUserBackend || showConfig.showStoragePath" |
||||
class="header__cell header__cell--large" |
||||
scope="col"> |
||||
<span v-if="showConfig.showUserBackend"> |
||||
{{ t('settings', 'User backend') }} |
||||
</span> |
||||
<span v-if="showConfig.showStoragePath" |
||||
class="header__subtitle"> |
||||
{{ t('settings', 'Storage location') }} |
||||
</span> |
||||
</th> |
||||
<th v-if="showConfig.showLastLogin" |
||||
class="header__cell" |
||||
scope="col"> |
||||
<span>{{ t('settings', 'Last login') }}</span> |
||||
</th> |
||||
<th class="header__cell header__cell--large" |
||||
scope="col"> |
||||
<span>{{ t('settings', 'Manager') }}</span> |
||||
</th> |
||||
<th class="header__cell header__cell--actions" |
||||
scope="col"> |
||||
<span class="hidden-visually"> |
||||
{{ t('settings', 'User actions') }} |
||||
</span> |
||||
</th> |
||||
</tr> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import Vue from 'vue' |
||||
|
||||
import { translate as t } from '@nextcloud/l10n' |
||||
|
||||
export default Vue.extend({ |
||||
name: 'UserListHeader', |
||||
|
||||
props: { |
||||
hasObfuscated: { |
||||
type: Boolean, |
||||
required: true, |
||||
}, |
||||
}, |
||||
|
||||
computed: { |
||||
showConfig() { |
||||
// @ts-expect-error: allow untyped $store |
||||
return this.$store.getters.getShowConfig |
||||
}, |
||||
|
||||
settings() { |
||||
// @ts-expect-error: allow untyped $store |
||||
return this.$store.getters.getServerData |
||||
}, |
||||
|
||||
subAdminsGroups() { |
||||
// @ts-expect-error: allow untyped $store |
||||
return this.$store.getters.getSubadminGroups |
||||
}, |
||||
|
||||
passwordLabel(): string { |
||||
if (this.hasObfuscated) { |
||||
return t('settings', 'Password or insufficient permissions message') |
||||
} |
||||
return t('settings', 'Password') |
||||
}, |
||||
}, |
||||
|
||||
methods: { |
||||
t, |
||||
}, |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
@import './shared/styles.scss'; |
||||
|
||||
.header { |
||||
@include row; |
||||
@include cell; |
||||
|
||||
border-bottom: 1px solid var(--color-border); |
||||
} |
||||
</style> |
File diff suppressed because it is too large
Load Diff
@ -1,185 +0,0 @@ |
||||
<template> |
||||
<div class="row" |
||||
:class="{'disabled': loading.delete || loading.disable}" |
||||
:data-id="user.id"> |
||||
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"> |
||||
<img v-if="!loading.delete && !loading.disable && !loading.wipe" |
||||
alt="" |
||||
width="32" |
||||
height="32" |
||||
:src="generateAvatar(user.id, isDarkTheme)"> |
||||
</div> |
||||
<!-- dirty hack to ellipsis on two lines --> |
||||
<div class="name"> |
||||
<div class="displayName subtitle"> |
||||
<div :title="user.displayname.length > 20 ? user.displayname : ''" class="cellText"> |
||||
<strong> |
||||
{{ user.displayname }} |
||||
</strong> |
||||
</div> |
||||
</div> |
||||
{{ user.id }} |
||||
</div> |
||||
<div /> |
||||
<div class="mailAddress"> |
||||
<div :title="user.email !== null && user.email.length > 20 ? user.email : ''" class="cellText"> |
||||
{{ user.email }} |
||||
</div> |
||||
</div> |
||||
<div class="groups"> |
||||
{{ userGroupsLabels }} |
||||
</div> |
||||
<div v-if="subAdminsGroups.length > 0 && settings.isAdmin" class="subAdminsGroups"> |
||||
{{ userSubAdminsGroupsLabels }} |
||||
</div> |
||||
<div class="userQuota"> |
||||
<div class="quota"> |
||||
{{ userQuota }} ({{ usedSpace }}) |
||||
<progress class="quota-user-progress" |
||||
:class="{'warn': usedQuota > 80}" |
||||
:value="usedQuota" |
||||
max="100" /> |
||||
</div> |
||||
</div> |
||||
<div v-if="showConfig.showLanguages" class="languages"> |
||||
{{ userLanguage.name }} |
||||
</div> |
||||
<div v-if="showConfig.showUserBackend || showConfig.showStoragePath" class="userBackend"> |
||||
<div v-if="showConfig.showUserBackend" class="userBackend"> |
||||
{{ user.backend }} |
||||
</div> |
||||
<div v-if="showConfig.showStoragePath" :title="user.storageLocation" class="storageLocation subtitle"> |
||||
{{ user.storageLocation }} |
||||
</div> |
||||
</div> |
||||
<div v-if="showConfig.showLastLogin" :title="userLastLoginTooltip" class="lastLogin"> |
||||
{{ userLastLogin }} |
||||
</div> |
||||
<div class="managers"> |
||||
{{ user.manager }} |
||||
</div> |
||||
<div class="userActions"> |
||||
<UserRowActions v-if="canEdit && !loading.all" |
||||
:actions="userActions" |
||||
:edit="false" |
||||
@update:edit="toggleEdit" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { getCurrentUser } from '@nextcloud/auth' |
||||
|
||||
import ClickOutside from 'vue-click-outside' |
||||
|
||||
import UserRowActions from './UserRowActions.vue' |
||||
import UserRowMixin from '../../mixins/UserRowMixin.js' |
||||
|
||||
export default { |
||||
name: 'UserRowSimple', |
||||
components: { |
||||
UserRowActions, |
||||
}, |
||||
directives: { |
||||
ClickOutside, |
||||
}, |
||||
mixins: [UserRowMixin], |
||||
props: { |
||||
user: { |
||||
type: Object, |
||||
required: true, |
||||
}, |
||||
loading: { |
||||
type: Object, |
||||
required: true, |
||||
}, |
||||
showConfig: { |
||||
type: Object, |
||||
required: true, |
||||
}, |
||||
userActions: { |
||||
type: Array, |
||||
required: true, |
||||
}, |
||||
openedMenu: { |
||||
type: Boolean, |
||||
required: true, |
||||
}, |
||||
subAdminsGroups: { |
||||
type: Array, |
||||
required: true, |
||||
}, |
||||
settings: { |
||||
type: Object, |
||||
required: true, |
||||
}, |
||||
isDarkTheme: { |
||||
type: Boolean, |
||||
required: true, |
||||
}, |
||||
}, |
||||
computed: { |
||||
userGroupsLabels() { |
||||
return this.userGroups |
||||
.map(group => group.name) |
||||
.join(', ') |
||||
}, |
||||
userSubAdminsGroupsLabels() { |
||||
return this.userSubAdminsGroups |
||||
.map(group => group.name) |
||||
.join(', ') |
||||
}, |
||||
usedSpace() { |
||||
if (this.user.quota.used) { |
||||
return t('settings', '{size} used', { size: OC.Util.humanFileSize(this.user.quota.used) }) |
||||
} |
||||
return t('settings', '{size} used', { size: OC.Util.humanFileSize(0) }) |
||||
}, |
||||
canEdit() { |
||||
return getCurrentUser().uid !== this.user.id || this.settings.isAdmin |
||||
}, |
||||
userQuota() { |
||||
let quota = this.user.quota.quota |
||||
|
||||
if (quota === 'default') { |
||||
quota = this.settings.defaultQuota |
||||
if (quota !== 'none') { |
||||
// convert to numeric value to match what the server would usually return |
||||
quota = OC.Util.computerFileSize(quota) |
||||
} |
||||
} |
||||
|
||||
// when the default quota is unlimited, the server returns -3 here, map it to "none" |
||||
if (quota === 'none' || quota === -3) { |
||||
return t('settings', 'Unlimited') |
||||
} else if (quota >= 0) { |
||||
return OC.Util.humanFileSize(quota) |
||||
} |
||||
return OC.Util.humanFileSize(0) |
||||
}, |
||||
}, |
||||
methods: { |
||||
toggleMenu() { |
||||
this.$emit('update:openedMenu', !this.openedMenu) |
||||
}, |
||||
hideMenu() { |
||||
this.$emit('update:openedMenu', false) |
||||
}, |
||||
toggleEdit() { |
||||
this.$emit('update:editing', true) |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.cellText { |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
white-space: nowrap; |
||||
} |
||||
.icon-more { |
||||
background-color: var(--color-main-background); |
||||
border: 0; |
||||
} |
||||
</style> |
@ -0,0 +1,110 @@ |
||||
/** |
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com> |
||||
* |
||||
* @author Christopher Ng <chrng8@gmail.com> |
||||
* |
||||
* @license AGPL-3.0-or-later |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
|
||||
@mixin row { |
||||
position: absolute; |
||||
display: flex; |
||||
height: var(--row-height); |
||||
background-color: var(--color-main-background); |
||||
} |
||||
|
||||
@mixin cell { |
||||
&__cell { |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
padding: 0 var(--cell-padding); |
||||
width: var(--cell-width); |
||||
color: var(--color-main-text); |
||||
|
||||
strong, |
||||
span, |
||||
label { |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
white-space: nowrap; |
||||
overflow-wrap: anywhere; |
||||
} |
||||
|
||||
@media (min-width: 670px) { /* Show one &--large column between stickied columns */ |
||||
&--avatar, |
||||
&--displayname { |
||||
position: sticky; |
||||
z-index: 10; |
||||
background-color: var(--color-main-background); |
||||
} |
||||
|
||||
&--avatar { |
||||
left: 0; |
||||
} |
||||
|
||||
&--displayname { |
||||
left: var(--avatar-cell-width); |
||||
border-right: 1px solid var(--color-border); |
||||
} |
||||
} |
||||
|
||||
&--avatar { |
||||
width: var(--avatar-cell-width); |
||||
align-items: center; |
||||
padding: 0; |
||||
user-select: none; |
||||
} |
||||
|
||||
&--multiline { |
||||
span { |
||||
line-height: 1.3em; |
||||
white-space: unset; |
||||
|
||||
@supports (-webkit-line-clamp: 2) { |
||||
display: -webkit-box; |
||||
-webkit-line-clamp: 2; |
||||
-webkit-box-orient: vertical; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&--large { |
||||
width: 300px; |
||||
} |
||||
|
||||
&--obfuscated { |
||||
width: 400px; |
||||
} |
||||
|
||||
&--actions { |
||||
position: sticky; |
||||
right: 0; |
||||
z-index: 10; |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
width: 110px; |
||||
background-color: var(--color-main-background); |
||||
border-left: 1px solid var(--color-border); |
||||
} |
||||
} |
||||
|
||||
&__subtitle { |
||||
color: var(--color-text-maxcontrast); |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
/** |
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com> |
||||
* |
||||
* @author Christopher Ng <chrng8@gmail.com> |
||||
* |
||||
* @license AGPL-3.0-or-later |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
|
||||
export const unlimitedQuota = { |
||||
id: 'none', |
||||
label: t('settings', 'Unlimited'), |
||||
} |
||||
|
||||
export const defaultQuota = { |
||||
id: 'default', |
||||
label: t('settings', 'Default quota'), |
||||
} |
||||
|
||||
/** |
||||
* Return `true` if the logged in user does not have permissions to view the |
||||
* data of `user` |
||||
*/ |
||||
export const isObfuscated = (user: { id: string, [key: string]: any }) => { |
||||
const keys = Object.keys(user) |
||||
return keys.length === 1 && keys.at(0) === 'id' |
||||
} |
Loading…
Reference in new issue