mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
reusable dialogs
This commit is contained in:
parent
c1226c1184
commit
c78240c832
10 changed files with 188 additions and 72 deletions
|
|
@ -3,6 +3,8 @@
|
|||
<router-view />
|
||||
|
||||
<SnackQueue />
|
||||
<DialogInstanceConfirmEdit />
|
||||
<DialogInstanceConfirm />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
3
next-ui/src/components.d.ts
vendored
3
next-ui/src/components.d.ts
vendored
|
|
@ -23,10 +23,13 @@ declare module 'vue' {
|
|||
BuildVersion: typeof import('./components/BuildVersion.vue')['default']
|
||||
DialogConfirm: typeof import('./components/dialog/Confirm.vue')['default']
|
||||
DialogConfirmEdit: typeof import('./components/dialog/ConfirmEdit.vue')['default']
|
||||
DialogInstanceConfirm: typeof import('./components/dialog/instance/Confirm.vue')['default']
|
||||
DialogInstanceConfirmEdit: typeof import('./components/dialog/instance/ConfirmEdit.vue')['default']
|
||||
FormUserChangePassword: typeof import('./components/form/user/ChangePassword.vue')['default']
|
||||
FormUserEdit: typeof import('./components/form/user/Edit.vue')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
|
||||
NoticeUserDeletion: typeof import('./components/notice/UserDeletion.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SnackQueue: typeof import('./components/SnackQueue.vue')['default']
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ function submitForm() {
|
|||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
export interface DialogConfirmProps {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
okText?: string
|
||||
|
|
@ -97,7 +97,7 @@ const {
|
|||
validateText = 'confirm',
|
||||
maxWidth = undefined,
|
||||
activator = undefined,
|
||||
} = defineProps<Props>()
|
||||
} = defineProps<DialogConfirmProps>()
|
||||
|
||||
function close() {
|
||||
showDialog.value = false
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function submitForm(callback: () => void) {
|
|||
if (formValid.value) callback()
|
||||
}
|
||||
|
||||
interface Props {
|
||||
export interface DialogConfirmEditProps {
|
||||
/**
|
||||
* Dialog title
|
||||
* @type string
|
||||
|
|
@ -84,7 +84,7 @@ const {
|
|||
subtitle = undefined,
|
||||
maxWidth = undefined,
|
||||
activator = undefined,
|
||||
} = defineProps<Props>()
|
||||
} = defineProps<DialogConfirmEditProps>()
|
||||
|
||||
function close() {
|
||||
showDialog.value = false
|
||||
|
|
|
|||
27
next-ui/src/components/dialog/instance/Confirm.vue
Normal file
27
next-ui/src/components/dialog/instance/Confirm.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<DialogConfirm
|
||||
v-bind="confirm.dialogProps"
|
||||
:activator="confirm.activator"
|
||||
@confirm="confirm.confirmCallback()"
|
||||
>
|
||||
<template #warning>
|
||||
<component
|
||||
:is="confirm.slotWarning.component"
|
||||
v-bind="confirm.slotWarning.props"
|
||||
/>
|
||||
</template>
|
||||
</DialogConfirm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Single instance of DialogConfirm, mounted under App.
|
||||
* Communication from other components is done via useDialogsStore.confirm
|
||||
*/
|
||||
import { useDialogsStore } from '@/stores/dialogs'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const { confirm } = storeToRefs(useDialogsStore())
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
29
next-ui/src/components/dialog/instance/ConfirmEdit.vue
Normal file
29
next-ui/src/components/dialog/instance/ConfirmEdit.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<DialogConfirmEdit
|
||||
v-bind="confirmEdit.dialogProps"
|
||||
:activator="confirmEdit.activator"
|
||||
v-model:record="confirmEdit.record"
|
||||
@update:record="confirmEdit.recordUpdatedCallback()"
|
||||
>
|
||||
<template #text="{ proxyModel }">
|
||||
<component
|
||||
:is="confirmEdit.slot.component"
|
||||
v-bind="confirmEdit.slot.props"
|
||||
v-model="proxyModel.value"
|
||||
/>
|
||||
</template>
|
||||
</DialogConfirmEdit>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Single instance of DialogConfirmEdit, mounted under App.
|
||||
* Communication from other components is done via useDialogsStore.confirmEdit
|
||||
*/
|
||||
import { useDialogsStore } from '@/stores/dialogs'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const { confirmEdit } = storeToRefs(useDialogsStore())
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
1
next-ui/src/components/notice/README.md
Normal file
1
next-ui/src/components/notice/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Components that can be used within a `DialogConfirm`.
|
||||
16
next-ui/src/components/notice/UserDeletion.vue
Normal file
16
next-ui/src/components/notice/UserDeletion.vue
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<v-alert
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<div>The user account will be deleted from this server.</div>
|
||||
<ul class="ps-8">
|
||||
<li>The read progress for this user account will be permanently deleted.</li>
|
||||
<li>Authentication activity for this user will be permanently deleted.</li>
|
||||
</ul>
|
||||
<div class="font-weight-bold mt-4">This action cannot be undone.</div>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
text="Add a User"
|
||||
border
|
||||
@click="showDialog(ACTION.ADD)"
|
||||
@mouseenter="activator = $event.currentTarget"
|
||||
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
|
||||
/>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
|
@ -56,66 +56,25 @@
|
|||
v-tooltip:bottom="'Change password'"
|
||||
:icon="mdiLockReset"
|
||||
@click="showDialog(ACTION.PASSWORD, user)"
|
||||
@mouseenter="activator = $event.currentTarget"
|
||||
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
|
||||
/>
|
||||
<v-icon-btn
|
||||
v-tooltip:bottom="'Edit user'"
|
||||
:icon="mdiPencil"
|
||||
:disabled="me?.id == user.id"
|
||||
@click="showDialog(ACTION.EDIT, user)"
|
||||
@mouseenter="activator = $event.currentTarget"
|
||||
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
|
||||
/>
|
||||
<v-icon-btn
|
||||
v-tooltip:bottom="'Delete user'"
|
||||
:icon="mdiDelete"
|
||||
:disabled="me?.id == user.id"
|
||||
@click="showDialog(ACTION.DELETE, user)"
|
||||
@mouseenter="activatorDelete = $event.currentTarget"
|
||||
@mouseenter="dialogConfirm.activator = $event.currentTarget"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<DialogConfirmEdit
|
||||
v-model:record="dialogRecord"
|
||||
:activator="activator"
|
||||
:title="dialogTitle"
|
||||
:subtitle="userRecord?.email"
|
||||
:max-width="currentAction === ACTION.PASSWORD ? 400 : 600"
|
||||
@update:record="handleDialogConfirmation()"
|
||||
>
|
||||
<template #text="{ proxyModel }">
|
||||
<component
|
||||
:is="dialogComponent"
|
||||
v-model="proxyModel.value"
|
||||
/>
|
||||
</template>
|
||||
</DialogConfirmEdit>
|
||||
|
||||
<DialogConfirm
|
||||
:activator="activatorDelete"
|
||||
:title="dialogTitle"
|
||||
:subtitle="userRecord?.email"
|
||||
ok-text="Delete"
|
||||
:validate-text="userRecord?.email"
|
||||
max-width="600"
|
||||
@confirm="handleDialogConfirmation()"
|
||||
>
|
||||
<template #warning>
|
||||
<v-alert
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<div>The user account will be deleted from this server.</div>
|
||||
<ul class="ps-8">
|
||||
<li>The read progress for this user account will be permanently deleted.</li>
|
||||
<li>Authentication activity for this user will be permanently deleted.</li>
|
||||
</ul>
|
||||
<div class="font-weight-bold mt-4">This action cannot be undone.</div>
|
||||
</v-alert>
|
||||
</template>
|
||||
</DialogConfirm>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
|
@ -138,9 +97,11 @@ import {
|
|||
} from '@/colada/mutations/update-user'
|
||||
import FormUserChangePassword from '@/components/form/user/ChangePassword.vue'
|
||||
import FormUserEdit from '@/components/form/user/Edit.vue'
|
||||
import type { Component } from 'vue'
|
||||
import NoticeUserDeletion from '@/components/notice/UserDeletion.vue'
|
||||
import { useLibraries } from '@/colada/queries/libraries'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDialogsStore } from '@/stores/dialogs'
|
||||
|
||||
// API data
|
||||
const { data: users, error, isLoading, refetch: refetchUsers } = useUsers()
|
||||
|
|
@ -193,13 +154,8 @@ onMounted(() => refetchUsers())
|
|||
const userRecord = ref<components['schemas']['UserDto']>()
|
||||
// stores the ongoing action, so we can handle the action when the dialog is closed with changes
|
||||
const currentAction = ref<ACTION>()
|
||||
// the record passed to the dialog's form's model
|
||||
const dialogRecord = ref<unknown>()
|
||||
const activator = ref<Element>()
|
||||
const activatorDelete = ref<Element>()
|
||||
const dialogTitle = ref<string>()
|
||||
// dynamic component for the dialog's inner form
|
||||
const dialogComponent = shallowRef<Component>()
|
||||
|
||||
const { confirmEdit: dialogConfirmEdit, confirm: dialogConfirm } = storeToRefs(useDialogsStore())
|
||||
|
||||
const { mutate: mutateCreateUser } = useCreateUser()
|
||||
const { mutate: mutateUser } = useUpdateUser()
|
||||
|
|
@ -218,9 +174,15 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
|
|||
currentAction.value = action
|
||||
switch (action) {
|
||||
case ACTION.ADD:
|
||||
dialogTitle.value = 'Add User'
|
||||
dialogComponent.value = FormUserEdit
|
||||
dialogRecord.value = {
|
||||
dialogConfirmEdit.value.dialogProps = {
|
||||
title: 'Add User',
|
||||
maxWidth: 600,
|
||||
}
|
||||
dialogConfirmEdit.value.slot = {
|
||||
component: markRaw(FormUserEdit),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirmEdit.value.record = {
|
||||
email: '',
|
||||
password: '',
|
||||
roles: [UserRoles.PAGE_STREAMING, UserRoles.FILE_DOWNLOAD],
|
||||
|
|
@ -234,11 +196,19 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
|
|||
restriction: 'NONE',
|
||||
},
|
||||
} as components['schemas']['UserCreationDto']
|
||||
dialogConfirmEdit.value.recordUpdatedCallback = handleDialogConfirmation
|
||||
break
|
||||
case ACTION.EDIT:
|
||||
dialogTitle.value = 'Edit User'
|
||||
dialogComponent.value = FormUserEdit
|
||||
dialogRecord.value = {
|
||||
dialogConfirmEdit.value.dialogProps = {
|
||||
title: 'Edit User',
|
||||
subtitle: user?.email,
|
||||
maxWidth: 600,
|
||||
}
|
||||
dialogConfirmEdit.value.slot = {
|
||||
component: markRaw(FormUserEdit),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirmEdit.value.record = {
|
||||
...user,
|
||||
roles: user?.roles.filter((x) => x !== 'USER'),
|
||||
sharedLibraries: {
|
||||
|
|
@ -253,17 +223,35 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
|
|||
restriction: 'NONE',
|
||||
},
|
||||
} as components['schemas']['UserUpdateDto']
|
||||
dialogConfirmEdit.value.recordUpdatedCallback = handleDialogConfirmation
|
||||
break
|
||||
case ACTION.DELETE:
|
||||
dialogTitle.value = 'Delete User'
|
||||
dialogComponent.value = FormUserEdit
|
||||
dialogRecord.value = user
|
||||
dialogConfirm.value.dialogProps = {
|
||||
title: 'Delete User',
|
||||
subtitle: user?.email,
|
||||
maxWidth: 600,
|
||||
validateText: user?.email,
|
||||
okText: 'Delete',
|
||||
}
|
||||
dialogConfirm.value.slotWarning = {
|
||||
component: markRaw(NoticeUserDeletion),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirm.value.confirmCallback = handleDialogConfirmation
|
||||
break
|
||||
case ACTION.PASSWORD:
|
||||
dialogTitle.value = 'Change Password'
|
||||
dialogComponent.value = FormUserChangePassword
|
||||
dialogConfirmEdit.value.dialogProps = {
|
||||
title: 'Change Password',
|
||||
subtitle: user?.email,
|
||||
maxWidth: 400,
|
||||
}
|
||||
dialogConfirmEdit.value.slot = {
|
||||
component: markRaw(FormUserChangePassword),
|
||||
props: {},
|
||||
}
|
||||
// password change initiated with an empty string
|
||||
dialogRecord.value = ''
|
||||
dialogConfirmEdit.value.record = ''
|
||||
dialogConfirmEdit.value.recordUpdatedCallback = handleDialogConfirmation
|
||||
}
|
||||
userRecord.value = user
|
||||
}
|
||||
|
|
@ -271,10 +259,10 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
|
|||
function handleDialogConfirmation() {
|
||||
switch (currentAction.value) {
|
||||
case ACTION.ADD:
|
||||
mutateCreateUser(dialogRecord.value as components['schemas']['UserCreationDto'])
|
||||
mutateCreateUser(dialogConfirmEdit.value.record as components['schemas']['UserCreationDto'])
|
||||
break
|
||||
case ACTION.EDIT:
|
||||
mutateUser(dialogRecord.value as components['schemas']['UserDto'])
|
||||
mutateUser(dialogConfirmEdit.value.record as components['schemas']['UserDto'])
|
||||
break
|
||||
case ACTION.DELETE:
|
||||
mutateDeleteUser(userRecord.value!.id)
|
||||
|
|
@ -282,7 +270,7 @@ function handleDialogConfirmation() {
|
|||
case ACTION.PASSWORD:
|
||||
mutateUserPassword({
|
||||
userId: userRecord.value!.id,
|
||||
newPassword: dialogRecord.value as string,
|
||||
newPassword: dialogConfirmEdit.value.record as string,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
|
|
|||
50
next-ui/src/stores/dialogs.ts
Normal file
50
next-ui/src/stores/dialogs.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Utilities
|
||||
import { defineStore } from 'pinia'
|
||||
import type { DialogConfirmEditProps } from '@/components/dialog/ConfirmEdit.vue'
|
||||
import type { DialogConfirmProps } from '@/components/dialog/Confirm.vue'
|
||||
|
||||
/**
|
||||
* Reusable dialogs.
|
||||
* The single instances of the dialogs are created under App, and can be triggered by using this store.
|
||||
*/
|
||||
export const useDialogsStore = defineStore('dialogs', {
|
||||
state: () => ({
|
||||
confirmEdit: {
|
||||
dialogProps: {},
|
||||
slot: {
|
||||
component: undefined,
|
||||
props: {},
|
||||
},
|
||||
record: undefined,
|
||||
recordUpdatedCallback: () => {},
|
||||
} as DialogConfirmEditActivation,
|
||||
confirm: {
|
||||
dialogProps: {},
|
||||
slotWarning: {
|
||||
component: undefined,
|
||||
props: {},
|
||||
},
|
||||
confirmCallback: () => {},
|
||||
} as DialogConfirmActivation,
|
||||
}),
|
||||
})
|
||||
|
||||
interface DialogConfirmEditActivation {
|
||||
activator?: Element | string
|
||||
dialogProps: DialogConfirmEditProps
|
||||
slot: ComponentWithProps
|
||||
record?: unknown
|
||||
recordUpdatedCallback: () => void
|
||||
}
|
||||
|
||||
interface DialogConfirmActivation {
|
||||
activator?: Element | string
|
||||
dialogProps: DialogConfirmProps
|
||||
slotWarning: ComponentWithProps
|
||||
confirmCallback: () => void
|
||||
}
|
||||
|
||||
interface ComponentWithProps {
|
||||
component?: Component
|
||||
props: object
|
||||
}
|
||||
Loading…
Reference in a new issue