diff --git a/next-ui/src/App.vue b/next-ui/src/App.vue index d47aabfa..8fbd5f4d 100644 --- a/next-ui/src/App.vue +++ b/next-ui/src/App.vue @@ -3,6 +3,8 @@ + + diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts index 7da3029f..b83e2aae 100644 --- a/next-ui/src/components.d.ts +++ b/next-ui/src/components.d.ts @@ -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'] diff --git a/next-ui/src/components/dialog/Confirm.vue b/next-ui/src/components/dialog/Confirm.vue index 36bb4951..b2dc2663 100644 --- a/next-ui/src/components/dialog/Confirm.vue +++ b/next-ui/src/components/dialog/Confirm.vue @@ -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() +} = defineProps() function close() { showDialog.value = false diff --git a/next-ui/src/components/dialog/ConfirmEdit.vue b/next-ui/src/components/dialog/ConfirmEdit.vue index 488e0bf3..1ed8232b 100644 --- a/next-ui/src/components/dialog/ConfirmEdit.vue +++ b/next-ui/src/components/dialog/ConfirmEdit.vue @@ -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() +} = defineProps() function close() { showDialog.value = false diff --git a/next-ui/src/components/dialog/instance/Confirm.vue b/next-ui/src/components/dialog/instance/Confirm.vue new file mode 100644 index 00000000..c94170f2 --- /dev/null +++ b/next-ui/src/components/dialog/instance/Confirm.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/next-ui/src/components/dialog/instance/ConfirmEdit.vue b/next-ui/src/components/dialog/instance/ConfirmEdit.vue new file mode 100644 index 00000000..257ec02c --- /dev/null +++ b/next-ui/src/components/dialog/instance/ConfirmEdit.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/next-ui/src/components/notice/README.md b/next-ui/src/components/notice/README.md new file mode 100644 index 00000000..f05ff680 --- /dev/null +++ b/next-ui/src/components/notice/README.md @@ -0,0 +1 @@ +Components that can be used within a `DialogConfirm`. diff --git a/next-ui/src/components/notice/UserDeletion.vue b/next-ui/src/components/notice/UserDeletion.vue new file mode 100644 index 00000000..d35fce4b --- /dev/null +++ b/next-ui/src/components/notice/UserDeletion.vue @@ -0,0 +1,16 @@ + + + diff --git a/next-ui/src/pages/server/users.vue b/next-ui/src/pages/server/users.vue index add112f6..a3564553 100644 --- a/next-ui/src/pages/server/users.vue +++ b/next-ui/src/pages/server/users.vue @@ -32,7 +32,7 @@ text="Add a User" border @click="showDialog(ACTION.ADD)" - @mouseenter="activator = $event.currentTarget" + @mouseenter="dialogConfirmEdit.activator = $event.currentTarget" /> @@ -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" /> - - - - - - - - @@ -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() // stores the ongoing action, so we can handle the action when the dialog is closed with changes const currentAction = ref() -// the record passed to the dialog's form's model -const dialogRecord = ref() -const activator = ref() -const activatorDelete = ref() -const dialogTitle = ref() -// dynamic component for the dialog's inner form -const dialogComponent = shallowRef() + +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 } diff --git a/next-ui/src/stores/dialogs.ts b/next-ui/src/stores/dialogs.ts new file mode 100644 index 00000000..e40b8e30 --- /dev/null +++ b/next-ui/src/stores/dialogs.ts @@ -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 +}