feat(xo6/user): show the username, first name and last name of users (#10657)

The users are now shown by their username, which replaces the deprecated
name property, or by their email address when they have none. The users
list and the user side panel also show their first and last names.
This commit is contained in:
Thierry Goettelmann
2026-11-09 08:13:13 +01:00
committed by GitHub
parent 35be9f2cc6
commit 25b837ef1a
16 changed files with 50 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
<template>
<UiTableCell>
<div class="user-name">
<div class="username">
<UiUserLogo size="small" />
<UiLink :to :href size="medium">
{{ content }}
@@ -23,7 +23,7 @@ defineProps<
</script>
<style scoped lang="postcss">
.user-name {
.username {
display: flex;
align-items: center;
gap: 0.8rem;

View File

@@ -1,11 +1,11 @@
import VtsUserNameCell from '@core/components/table/cells/VtsUserNameCell.vue'
import VtsUsernameCell from '@core/components/table/cells/VtsUsernameCell.vue'
import type { LinkOptions } from '@core/composables/link-component.composable.ts'
import { defineColumn } from '@core/packages/table/define-column.ts'
import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
import type { HeaderConfig } from '@core/tables/types.ts'
import { h } from 'vue'
export const useUserNameColumn = defineColumn((config?: HeaderConfig) => ({
export const useUsernameColumn = defineColumn((config?: HeaderConfig) => ({
renderHead: () => renderHeadCell(config?.headerLabel),
renderBody: (content: string, link?: Pick<LinkOptions, 'to' | 'href'>) => h(VtsUserNameCell, { content, ...link }),
renderBody: (content: string, link?: Pick<LinkOptions, 'to' | 'href'>) => h(VtsUsernameCell, { content, ...link }),
}))

View File

@@ -3,7 +3,7 @@ import { useActionColumn } from '@core/tables/column-definitions/action-column.t
import { useCheckboxColumn } from '@core/tables/column-definitions/checkbox-column.ts'
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
import { useTruncatedTextColumn } from '@core/tables/column-definitions/truncated-text-column.ts'
import { useUserNameColumn } from '@core/tables/column-definitions/user-name-column.ts'
import { useUsernameColumn } from '@core/tables/column-definitions/username-column.ts'
import { useI18n } from 'vue-i18n'
export const useUserColumns = defineColumns(() => {
@@ -11,7 +11,7 @@ export const useUserColumns = defineColumns(() => {
return {
checkbox: useCheckboxColumn(),
username: useUserNameColumn({ headerLabel: () => t('username') }),
username: useUsernameColumn({ headerLabel: () => t('username') }),
firstName: useTextColumn({ headerLabel: () => t('first-name') }),
lastName: useTextColumn({ headerLabel: () => t('last-name') }),
email: useTruncatedTextColumn({ headerLabel: () => t('email') }),

View File

@@ -36,6 +36,7 @@ import {
useUserQueryBuilder,
} from '@/modules/user/composables/use-user-query-builder.composable.ts'
import type { FrontXoUser } from '@/modules/user/remote-resources/use-xo-user-collection.ts'
import { getUserDisplayName } from '@/modules/user/utils/xo-user.util.ts'
import AssignmentsTableLayout from '@/shared/components/AssignmentsTableLayout.vue'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
@@ -112,7 +113,7 @@ const { HeadCells, BodyCells } = useUserColumns({
return {
checkbox: r => r({ selected: isSelected(user.id), onToggle: () => toggleSelection(user.id) }),
username: r => r(user.name, { to: { name: '/admin/user/[id]/general', params: { id: user.id } } }),
username: r => r(getUserDisplayName(user), { to: { name: '/admin/user/[id]/general', params: { id: user.id } } }),
firstName: r => r(user.firstname),
lastName: r => r(user.lastname),
email: r => r(user.email),

View File

@@ -10,7 +10,7 @@
size="small"
:to="{ name: '/admin/user-management/users', query: { id: user.id } }"
>
{{ user.name ?? user.email }}
{{ getUserDisplayName(user) }}
</UiLink>
</UiLinkList>
@@ -23,6 +23,7 @@
<script lang="ts" setup>
import type { FrontXoGroup } from '@/modules/group/remote-resources/use-xo-group-collection.ts'
import { useXoUserCollection } from '@/modules/user/remote-resources/use-xo-user-collection.ts'
import { getUserDisplayName } from '@/modules/user/utils/xo-user.util.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiLinkList from '@core/components/ui/link-list/UiLinkList.vue'

View File

@@ -36,6 +36,7 @@ import {
type FilterableUser,
useUserQueryBuilder,
} from '@/modules/user/composables/use-user-query-builder.composable.ts'
import { getUserDisplayName } from '@/modules/user/utils/xo-user.util.ts'
import AssignmentsTableLayout from '@/shared/components/AssignmentsTableLayout.vue'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
@@ -115,7 +116,7 @@ const { HeadCells, BodyCells } = useUserColumns({
return {
checkbox: r =>
user.isDirect ? r({ selected: isSelected(user.id), onToggle: () => toggleSelection(user.id) }) : r(),
username: r => r(user.name, { to: { name: '/admin/user/[id]/general', params: { id: user.id } } }),
username: r => r(getUserDisplayName(user), { to: { name: '/admin/user/[id]/general', params: { id: user.id } } }),
firstName: r => r(user.firstname),
lastName: r => r(user.lastname),
email: r => r(user.email),

View File

@@ -19,7 +19,7 @@
size="small"
:to="{ name: '/admin/user-management/users', query: { id: user.id } }"
>
{{ user.name ?? user.email }}
{{ getUserDisplayName(user) }}
</UiLink>
</UiLinkList>
</div>
@@ -36,6 +36,7 @@ import { useXoGroupCollection } from '@/modules/group/remote-resources/use-xo-gr
import type { FrontXoRole } from '@/modules/role/remote-resources/use-xo-role-collection.ts'
import { getRoleUserIds } from '@/modules/role/utils/xo-role.util.ts'
import { type FrontXoUser, useXoUserCollection } from '@/modules/user/remote-resources/use-xo-user-collection.ts'
import { getUserDisplayName } from '@/modules/user/utils/xo-user.util.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiLinkList from '@core/components/ui/link-list/UiLinkList.vue'

View File

@@ -4,7 +4,7 @@
{{ t('user-details') }}
</UiTitle>
<VtsTabularKeyValueList>
<VtsTabularKeyValueRow :label="t('username')" :value="user.name" />
<VtsTabularKeyValueRow :label="t('username')" :value="user.username" />
<VtsTabularKeyValueRow :label="t('uuid')" :value="user.id" />
<VtsTabularKeyValueRow :label="t('last-login')" :value="lastLogin" />
<VtsTabularKeyValueRow :label="t('first-name')" :value="user.firstname" />

View File

@@ -10,12 +10,12 @@
</UiLink>
<span class="name">
<UiUserLogo size="extra-small" />
{{ user.name }}
{{ getUserDisplayName(user) }}
</span>
</UiBreadcrumb>
</div>
<UiHeadBar>
{{ user.name }}
{{ getUserDisplayName(user) }}
<template #icon>
<UiUserLogo size="small" />
</template>
@@ -24,6 +24,7 @@
<script lang="ts" setup>
import type { FrontXoUser } from '@/modules/user/remote-resources/use-xo-user-collection.ts'
import { getUserDisplayName } from '@/modules/user/utils/xo-user.util.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import UiBreadcrumb from '@core/components/ui/breadcrumb/UiBreadcrumb.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'

View File

@@ -40,6 +40,7 @@ import {
useUserQueryBuilder,
} from '@/modules/user/composables/use-user-query-builder.composable.ts'
import type { FrontXoUser } from '@/modules/user/remote-resources/use-xo-user-collection.ts'
import { getUserDisplayName } from '@/modules/user/utils/xo-user.util.ts'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
@@ -114,7 +115,7 @@ const { HeadCells, BodyCells } = useUserColumns({
}),
body: (user: FilterableUser) => ({
checkbox: r => r({ selected: isSelected(user.id), onToggle: () => toggleSelection(user.id) }),
username: r => r(user.name),
username: r => r(getUserDisplayName(user)),
firstName: r => r(user.firstname),
lastName: r => r(user.lastname),
email: r => r(user.email),

View File

@@ -25,7 +25,7 @@
<script setup lang="ts">
import { useUserDelete } from '@/modules/user/composables/use-user-delete.composable.ts'
import type { FrontXoUser } from '@/modules/user/remote-resources/use-xo-user-collection.ts'
import { getUserProvidersLabel } from '@/modules/user/utils/xo-user.util.ts'
import { getUserDisplayName, getUserProvidersLabel } from '@/modules/user/utils/xo-user.util.ts'
import type { ActionItem } from '@core/components/menu/VtsActionsMenu.vue'
import VtsTableBulkActions from '@core/components/table/VtsTableBulkActions.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
@@ -100,7 +100,6 @@ const state = useTableState({
})
const { HeadCells, BodyCells } = useUserColumns({
exclude: ['firstName', 'lastName'],
head: () => ({
checkbox: r => r(pageSelectionModel),
}),
@@ -109,7 +108,9 @@ const { HeadCells, BodyCells } = useUserColumns({
return {
checkbox: r => r({ selected: isSelected(user.id), onToggle: () => toggleSelection(user.id) }),
username: r => r(user.name ?? '', { to: { name: '/admin/user/[id]/general', params: { id: user.id } } }),
username: r => r(getUserDisplayName(user), { to: { name: '/admin/user/[id]/general', params: { id: user.id } } }),
firstName: r => r(user.firstname ?? ''),
lastName: r => r(user.lastname ?? ''),
email: r => r(user.email),
providers: r => r(getUserProvidersLabel(user, t('local'))),
actions: r =>

View File

@@ -5,7 +5,7 @@
<span class="name">
<UiUserLogo size="small" />
<UiLink :to="{ name: '/admin/user/[id]/general', params: { id: user.id } }" size="medium">
{{ user.name }}
{{ getUserDisplayName(user) }}
</UiLink>
</span>
</template>
@@ -25,6 +25,20 @@
<VtsCopyButton :value="lastLogin" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('first-name') }}</template>
<template #value>{{ user.firstname }}</template>
<template v-if="user.firstname" #addons>
<VtsCopyButton :value="user.firstname" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('last-name') }}</template>
<template #value>{{ user.lastname }}</template>
<template v-if="user.lastname" #addons>
<VtsCopyButton :value="user.lastname" />
</template>
</VtsCardRowKeyValue>
<VtsCardRowKeyValue truncate align-top>
<template #key>{{ t('email') }}</template>
<template #value>{{ user.email }}</template>
@@ -43,6 +57,7 @@
<script lang="ts" setup>
import { useXoUserUtils } from '@/modules/user/composables/use-xo-user-utils.composable.ts'
import type { FrontXoUser } from '@/modules/user/remote-resources/use-xo-user-collection.ts'
import { getUserDisplayName } from '@/modules/user/utils/xo-user.util.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'

View File

@@ -7,7 +7,7 @@ import { computed, type MaybeRefOrGetter, toValue } from 'vue'
import { useI18n } from 'vue-i18n'
export type FilterableUser<TUser extends FrontXoUser = FrontXoUser> = TUser & {
name: string
username: string
firstname: string
lastname: string
providers: string
@@ -23,7 +23,7 @@ export function useUserQueryBuilder<TUser extends FrontXoUser>(
const filterableUsers = computed<FilterableUser<TUser>[]>(() =>
toValue(users).map(user => ({
...user,
name: user.name ?? '',
username: user.username ?? '',
firstname: user.firstname ?? '',
lastname: user.lastname ?? '',
providers: getUserProvidersLabel(user, t('local')),
@@ -34,7 +34,7 @@ export function useUserQueryBuilder<TUser extends FrontXoUser>(
const schema = useQueryBuilderSchema<FilterableUser>({
'': useStringSchema(t('any-property')),
name: useStringSchema(t('username')),
username: useStringSchema(t('username')),
firstname: useStringSchema(t('first-name')),
lastname: useStringSchema(t('last-name')),
email: useStringSchema(t('email')),

View File

@@ -13,6 +13,7 @@ const userFields = [
'email',
'firstname',
'lastname',
'username',
'groups',
'authProviders',
] as const satisfies readonly (keyof XoUser)[]

View File

@@ -6,6 +6,10 @@ export function getUserProvidersLabel(user: Pick<XoUser, 'authProviders'>, local
return providerNames.length === 0 ? localLabel : providerNames.join(', ')
}
export function getUserDisplayName(user: Pick<XoUser, 'username' | 'email'>) {
return user.username || user.email
}
export function getUserLastLoginTimestamp(tokens: XoAuthenticationToken[]) {
let mostRecentTimestamp: number | undefined

View File

@@ -31,6 +31,7 @@
- [XO6/Roles] List the privileges of a role in its detail page, and see the objects each privilege currently applies to in a side panel
- [XO6/Privileges] Open a privilege's detail page from the privileges table of a role or its side panel, to see its details
- [XO6/Privileges] See the scope of a privilege in its detail page: its scope type, its query and the objects it currently applies to
- [XO6/Users] Show users by their username, or their email address when they have none, and show their first and last names in the users list and side panel
### Bug fixes