account details screen

This commit is contained in:
Gauthier Roebroeck 2025-07-21 15:27:13 +08:00
parent baf547d5a7
commit 0336dbb4fc
9 changed files with 231 additions and 7 deletions

View file

@ -37,6 +37,7 @@ declare module 'vue' {
RouterView: typeof import('vue-router')['RouterView']
ServerSettings: typeof import('./components/server/Settings.vue')['default']
UserDeletionWarning: typeof import('./components/user/DeletionWarning.vue')['default']
UserDetails: typeof import('./components/user/Details.vue')['default']
UserFormChangePassword: typeof import('./components/user/form/ChangePassword.vue')['default']
}
}

View file

@ -0,0 +1,11 @@
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as Stories from './Details.stories';
<Meta of={Stories} />
# UserDetails
Displays user information.
<Canvas of={Stories.Default} />

View file

@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Details from './Details.vue'
const meta = {
component: Details,
render: (args: object) => ({
components: { Details },
setup() {
return { args }
},
template: '<Details :user="args.user" />',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
},
args: {
user: {
id: '0JEDA00AV4Z7G',
email: 'admin@example.org',
roles: ['ADMIN', 'FILE_DOWNLOAD', 'KOBO_SYNC', 'KOREADER_SYNC', 'PAGE_STREAMING', 'USER'],
sharedAllLibraries: true,
sharedLibrariesIds: [],
labelsAllow: [],
labelsExclude: [],
},
},
} satisfies Meta<typeof Details>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View file

@ -0,0 +1,38 @@
<template>
<v-container class="pa-0">
<v-row align="center">
<v-col cols="auto">
<v-avatar
color="primary"
size="x-large"
><span class="text-h5 text-uppercase">{{ user.email.charAt(0) }}</span></v-avatar
>
</v-col>
<v-col>
{{ user.email }}
</v-col>
</v-row>
<v-row>
<v-col>
<div class="d-flex ga-1 flex-wrap">
<v-chip
v-for="role in user.roles"
:key="role"
:text="$formatMessage(userRolesMessages[role as UserRoles])"
size="small"
rounded
/>
</div>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import type { components } from '@/generated/openapi/komga'
import { UserRoles, userRolesMessages } from '@/types/UserRoles'
const { user } = defineProps<{
user: components['schemas']['UserDto']
}>()
</script>

View file

@ -0,0 +1,11 @@
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as Stories from './details.stories';
<Meta of={Stories} />
# Account Details
Page showing the current user's details.
<Canvas of={Stories.Default} />

View file

@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import AccountDetails from './details.vue'
const meta = {
component: AccountDetails,
render: (args: object) => ({
components: { AccountDetails },
setup() {
return { args }
},
template: '<AccountDetails />',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
},
args: {},
} satisfies Meta<typeof AccountDetails>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}

View file

@ -1,7 +1,89 @@
<template>
<h1>Account Details</h1>
<v-empty-state
v-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<template v-else-if="currentUser">
<UserDetails :user="currentUser" />
<v-btn
:text="
$formatMessage({
description: 'User details screen: change password button',
defaultMessage: 'Change password',
id: 'sGsWvI',
})
"
class="mt-8"
@click="changePassword()"
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
/>
</template>
</template>
<script lang="ts" setup>
//
import { useCurrentUser, useUpdateUserPassword } from '@/colada/users'
import { commonMessages } from '@/utils/i18n/common-messages'
import { storeToRefs } from 'pinia'
import { useDialogsStore } from '@/stores/dialogs'
import UserFormChangePassword from '@/components/user/form/ChangePassword.vue'
import type { ErrorCause } from '@/api/komga-client'
import { useMessagesStore } from '@/stores/messages'
import { useIntl } from 'vue-intl'
const intl = useIntl()
const { data: currentUser, error } = useCurrentUser()
const { mutateAsync: mutateUserPassword } = useUpdateUserPassword()
const { confirmEdit: dialogConfirmEdit } = storeToRefs(useDialogsStore())
const messagesStore = useMessagesStore()
function changePassword() {
dialogConfirmEdit.value.dialogProps = {
title: intl.formatMessage(commonMessages.changePasswordDialogTitle),
subtitle: currentUser.value?.email,
maxWidth: 400,
closeOnSave: false,
}
dialogConfirmEdit.value.slot = {
component: markRaw(UserFormChangePassword),
props: {},
}
// password change initiated with an empty string
dialogConfirmEdit.value.record = ''
dialogConfirmEdit.value.callback = handleDialogConfirmation
}
function handleDialogConfirmation(
hideDialog: () => void,
setLoading: (isLoading: boolean) => void,
) {
setLoading(true)
mutateUserPassword({
userId: currentUser.value!.id,
newPassword: dialogConfirmEdit.value.record as string,
})
?.then(() => {
hideDialog()
messagesStore.messages.push({
text: intl.formatMessage({
description:
"Snackbar notification shown upon successful current user's password modification",
defaultMessage: 'Password changed',
id: '0FEy0X',
}),
})
})
.catch((error) => {
messagesStore.messages.push({
text:
(error?.cause as ErrorCause)?.message || intl.formatMessage(commonMessages.networkError),
})
setLoading(false)
})
}
</script>

View file

@ -80,7 +80,11 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
switch (action) {
case ACTION.ADD:
dialogConfirmEdit.value.dialogProps = {
title: 'Add User',
title: intl.formatMessage({
description: 'Add user dialog title',
defaultMessage: 'Add User',
id: 'Bl30xt',
}),
maxWidth: 600,
closeOnSave: false,
fullscreen: display.xs.value,
@ -107,7 +111,11 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
break
case ACTION.EDIT:
dialogConfirmEdit.value.dialogProps = {
title: 'Edit User',
title: intl.formatMessage({
description: 'Edit user dialog title',
defaultMessage: 'Edit User',
id: 'Zh8AOV',
}),
subtitle: user?.email,
maxWidth: 600,
closeOnSave: false,
@ -136,11 +144,19 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
break
case ACTION.DELETE:
dialogConfirm.value.dialogProps = {
title: 'Delete User',
title: intl.formatMessage({
description: 'Delete user dialog title',
defaultMessage: 'Delete User',
id: '9XDmYO',
}),
subtitle: user?.email,
maxWidth: 600,
validateText: user?.email,
okText: 'Delete',
okText: intl.formatMessage({
description: 'Delete user dialog: confirmation button text',
defaultMessage: 'Delete',
id: 'o8WeX3',
}),
closeOnSave: false,
}
dialogConfirm.value.slotWarning = {
@ -151,7 +167,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
break
case ACTION.PASSWORD:
dialogConfirmEdit.value.dialogProps = {
title: 'Change Password',
title: intl.formatMessage(commonMessages.changePasswordDialogTitle),
subtitle: user?.email,
maxWidth: 400,
closeOnSave: false,

View file

@ -26,4 +26,9 @@ export const commonMessages = {
defaultMessage: 'Select an item or create one',
id: 'HXms0S',
}),
changePasswordDialogTitle: defineMessage({
description: 'Change Password dialog title',
defaultMessage: 'Change Password',
id: 'dHyAgE',
}),
}