From 80b99413997089f867b3c05b0db7c15ab199e7e2 Mon Sep 17 00:00:00 2001 From: jamesgeorge007 Date: Tue, 26 Mar 2024 22:39:38 +0530 Subject: [PATCH 1/6] chore: alert the user while deleting users who are team owners SH Admin user management. --- packages/hoppscotch-sh-admin/locales/en.json | 2 + .../hoppscotch-sh-admin/src/helpers/errors.ts | 3 + .../src/pages/users/_id.vue | 38 ++++-- .../src/pages/users/index.vue | 116 +++++++++++------- 4 files changed, 109 insertions(+), 50 deletions(-) diff --git a/packages/hoppscotch-sh-admin/locales/en.json b/packages/hoppscotch-sh-admin/locales/en.json index ad32101229..0427a80cb2 100644 --- a/packages/hoppscotch-sh-admin/locales/en.json +++ b/packages/hoppscotch-sh-admin/locales/en.json @@ -168,7 +168,9 @@ "remove_admin_from_users_failure": "Failed to remove admin status from selected users!!", "remove_admin_from_users_success": "Admin status removed from selected users!!", "remove_admin_to_delete_user": "Remove admin privilege to delete the user!!", + "remove_owner_to_delete_user": "Remove team ownership status to delete the user!!", "remove_admin_for_deletion": "Remove admin status before attempting deletion!!", + "remove_owner_for_deletion": "One or more users are team owners. Update ownership before deletion!!", "remove_invitee_failure": "Removal of invitee failed!!", "remove_invitee_success": "Removal of invitee is successfull!!", "remove_member_failure": "Member couldn't be removed!!", diff --git a/packages/hoppscotch-sh-admin/src/helpers/errors.ts b/packages/hoppscotch-sh-admin/src/helpers/errors.ts index cd45cea93f..d1ba9bdfe0 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/errors.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/errors.ts @@ -17,3 +17,6 @@ export const ADMIN_CANNOT_BE_DELETED = // When trying to invite a user that is already invited export const USER_ALREADY_INVITED = '[GraphQL] admin/user_already_invited' as const; + +// When attempting to delete a user who is an owner of a team +export const USER_IS_OWNER = 'user/is_owner' as const; diff --git a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue index d01c2c6f69..a4f68ce184 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue @@ -73,7 +73,7 @@ import { RemoveUsersByAdminDocument, UserInfoDocument, } from '~/helpers/backend/graphql'; -import { ADMIN_CANNOT_BE_DELETED } from '~/helpers/errors'; +import { ADMIN_CANNOT_BE_DELETED, USER_IS_OWNER } from '~/helpers/errors'; const t = useI18n(); const toast = useToast(); @@ -210,13 +210,35 @@ const deleteUserMutation = async (id: string | null) => { } else { const deletedUsers = result.data?.removeUsersByAdmin || []; - const isAdminError = deletedUsers.some( - (user) => user.errorMessage === ADMIN_CANNOT_BE_DELETED - ); - - isAdminError - ? toast.error(t('state.delete_user_failed_only_one_admin')) - : toast.success(t('state.delete_user_success')); + const uniqueErrorMessages = new Set( + deletedUsers.map(({ errorMessage }) => errorMessage).filter(Boolean) + ) as Set; + + if (uniqueErrorMessages.size > 0) { + const errMsgMap = { + [ADMIN_CANNOT_BE_DELETED]: t('state.remove_admin_to_delete_user'), + [USER_IS_OWNER]: t('state.remove_owner_to_delete_user'), + }; + + const errMsgMapKeys = Object.keys(errMsgMap); + + uniqueErrorMessages.forEach((errorMessage) => { + if (errMsgMapKeys.includes(errorMessage)) { + toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]); + } + }); + + // Fallback for the case where the error message is not in the compiled list + if ( + Array.from(uniqueErrorMessages).some( + (key) => !((key as string) in errMsgMap) + ) + ) { + toast.error(t('state.delete_user_failure')); + } + } else { + toast.success(t('state.delete_user_success')); + } } confirmDeletion.value = false; deleteUserUID.value = null; diff --git a/packages/hoppscotch-sh-admin/src/pages/users/index.vue b/packages/hoppscotch-sh-admin/src/pages/users/index.vue index b8b7e66c32..ac3f0a12c4 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/index.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/index.vue @@ -210,7 +210,7 @@ (null); const usersToAdmin = useMutation(MakeUsersAdminDocument); -const AreMultipleUsersSelected = computed(() => selectedRows.value.length > 1); +const areMultipleUsersSelected = computed(() => selectedRows.value.length > 1); const confirmUserToAdmin = (id: string | null) => { confirmUsersToAdmin.value = true; @@ -482,11 +482,15 @@ const makeUsersToAdmin = async (id: string | null) => { if (result.error) { toast.error( - id ? t('state.admin_failure') : t('state.users_to_admin_failure') + areMultipleUsersSelected.value + ? t('state.users_to_admin_failure') + : t('state.admin_failure') ); } else { toast.success( - id ? t('state.admin_success') : t('state.users_to_admin_success') + areMultipleUsersSelected.value + ? t('state.users_to_admin_success') + : t('state.admin_success') ); usersList.value = usersList.value.map((user) => ({ ...user, @@ -514,7 +518,7 @@ const resetConfirmAdminToUser = () => { adminsToUserUID.value = null; }; -const AreMultipleUsersSelectedToAdmin = computed( +const areMultipleUsersSelectedToAdmin = computed( () => selectedRows.value.length > 1 ); @@ -525,15 +529,15 @@ const makeAdminsToUsers = async (id: string | null) => { const result = await adminsToUser.executeMutation(variables); if (result.error) { toast.error( - id - ? t('state.remove_admin_failure') - : t('state.remove_admin_from_users_failure') + areMultipleUsersSelected.value + ? t('state.remove_admin_from_users_failure') + : t('state.remove_admin_failure') ); } else { toast.success( - id - ? t('state.remove_admin_success') - : t('state.remove_admin_from_users_success') + areMultipleUsersSelected.value + ? t('state.remove_admin_from_users_success') + : t('state.remove_admin_success') ); usersList.value = usersList.value.map((user) => ({ ...user, @@ -562,7 +566,7 @@ const resetConfirmUserDeletion = () => { deleteUserUID.value = null; }; -const AreMultipleUsersSelectedForDeletion = computed( +const areMultipleUsersSelectedForDeletion = computed( () => selectedRows.value.length > 1 ); @@ -572,12 +576,9 @@ const deleteUsers = async (id: string | null) => { const result = await usersDeletion.executeMutation(variables); if (result.error) { - const errorMessage = - result.error.message === DELETE_USER_FAILED_ONLY_ONE_ADMIN - ? t('state.delete_user_failed_only_one_admin') - : id - ? t('state.delete_user_failure') - : t('state.delete_users_failure'); + const errorMessage = areMultipleUsersSelected.value + ? t('state.delete_users_failure') + : t('state.delete_user_failure'); toast.error(errorMessage); } else { const deletedUsers = result.data?.removeUsersByAdmin || []; @@ -585,32 +586,63 @@ const deleteUsers = async (id: string | null) => { .filter((user) => user.isDeleted) .map((user) => user.userUID); - const isAdminError = deletedUsers.some( - (user) => user.errorMessage === ADMIN_CANNOT_BE_DELETED - ); - - usersList.value = usersList.value.filter( - (user) => !deletedIDs.includes(user.uid) - ); - - if (isAdminError) { - toast.success( - t('state.delete_some_users_success', { count: deletedIDs.length }) - ); - toast.error( - t('state.delete_some_users_failure', { - count: deletedUsers.length - deletedIDs.length, - }) - ); - toastTimeout = setTimeout(() => { - toast.error(t('state.remove_admin_for_deletion')); - }, 2000); + const uniqueErrorMessages = new Set( + deletedUsers.map(({ errorMessage }) => errorMessage).filter(Boolean) + ) as Set; + + if (uniqueErrorMessages.size > 0) { + const errMsgMap = { + [ADMIN_CANNOT_BE_DELETED]: t('state.remove_admin_for_deletion'), + [USER_IS_OWNER]: t('state.remove_owner_for_deletion'), + }; + + const errMsgMapKeys = Object.keys(errMsgMap); + + // Show toast messages with the count of users deleted only if multiple users are selected + if (areMultipleUsersSelected.value) { + toast.success( + t('state.delete_some_users_success', { count: deletedIDs.length }) + ); + toast.error( + t('state.delete_some_users_failure', { + count: deletedUsers.length - deletedIDs.length, + }) + ); + } + + uniqueErrorMessages.forEach((errorMessage) => { + if (errMsgMapKeys.includes(errorMessage)) { + toastTimeout = setTimeout( + () => { + toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]); + }, + areMultipleUsersSelected.value ? 2000 : 0 + ); + } + }); + + // Fallback for the case where the error message is not in the compiled list + if ( + Array.from(uniqueErrorMessages).some( + (key) => !((key as string) in errMsgMap) + ) + ) { + areMultipleUsersSelected.value + ? t('state.delete_users_failure') + : t('state.delete_user_failure'); + } } else { toast.success( - id ? t('state.delete_user_success') : t('state.delete_users_success') + areMultipleUsersSelected.value + ? t('state.delete_users_success') + : t('state.delete_user_success') ); } + usersList.value = usersList.value.filter( + (user) => !deletedIDs.includes(user.uid) + ); + selectedRows.value.splice(0, selectedRows.value.length); } confirmUsersDeletion.value = false; From 17db483a3505bc20b7a6ffea51a71c330359af4b Mon Sep 17 00:00:00 2001 From: jamesgeorge007 Date: Wed, 27 Mar 2024 13:05:32 +0530 Subject: [PATCH 2/6] refactor: leverage helpers --- .../src/helpers/userManagement.ts | 121 ++++++++++++++++++ .../hoppscotch-sh-admin/src/modules/i18n.ts | 21 ++- .../src/pages/users/_id.vue | 35 +---- .../src/pages/users/index.vue | 68 ++-------- 4 files changed, 158 insertions(+), 87 deletions(-) create mode 100644 packages/hoppscotch-sh-admin/src/helpers/userManagement.ts diff --git a/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts b/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts new file mode 100644 index 0000000000..662c32cd44 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts @@ -0,0 +1,121 @@ +import { useToast } from '~/composables/toast'; +import { getI18n } from '~/modules/i18n'; +import { UserDeletionResult } from './backend/graphql'; +import { ADMIN_CANNOT_BE_DELETED, USER_IS_OWNER } from './errors'; + +type IndividualActionInput = { + type: 'individual'; + metadata: null; +}; +type BulkActionInput = { + type: 'bulk'; + metadata: { + areMultipleUsersSelected: boolean; + deletedIDs: string[]; + }; +}; + +type IndividualActionResult = { + data: null; +}; +type BulkActionResult = { + data: { timeoutID: NodeJS.Timeout | null }; +}; + +type HandleUserDeletion = { + ( + deletedUsersList: UserDeletionResult[], + action: IndividualActionInput | BulkActionInput + ): IndividualActionResult | BulkActionResult; +}; + +const t = getI18n(); +const toast = useToast(); + +export const handleUserDeletion: HandleUserDeletion = ( + deletedUsersList, + action +) => { + let timeoutID: NodeJS.Timeout | null = null; + + const uniqueErrorMessages = new Set( + deletedUsersList.map(({ errorMessage }) => errorMessage).filter(Boolean) + ) as Set; + + const { type, metadata } = action; + + // Show the success toast based on the action type if there are no errors + if (uniqueErrorMessages.size === 0) { + if (type === 'bulk') { + toast.success( + metadata.areMultipleUsersSelected + ? t('state.delete_user_success') + : t('state.delete_users_success') + ); + + return { type, data: { timeoutID } }; + } + + toast.success(t('state.delete_user_success')); + return { type, data: null }; + } + + const errMsgMap = { + [ADMIN_CANNOT_BE_DELETED]: + type === 'bulk' + ? t('state.remove_admin_for_deletion') + : t('state.remove_admin_to_delete_user'), + + [USER_IS_OWNER]: + type === 'bulk' + ? t('state.remove_owner_for_deletion') + : t('state.remove_owner_to_delete_user'), + }; + const errMsgMapKeys = Object.keys(errMsgMap); + + if (type === 'bulk') { + const { areMultipleUsersSelected, deletedIDs } = metadata; + + // Show toast messages with the count of users deleted only if multiple users are selected + if (areMultipleUsersSelected) { + toast.success( + t('state.delete_some_users_success', { count: deletedIDs.length }) + ); + toast.error( + t('state.delete_some_users_failure', { + count: deletedUsersList.length - deletedIDs.length, + }) + ); + } + } + + uniqueErrorMessages.forEach((errorMessage) => { + if (errMsgMapKeys.includes(errorMessage)) { + if (type === 'bulk') { + timeoutID = setTimeout( + () => { + toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]); + }, + metadata.areMultipleUsersSelected ? 2000 : 0 + ); + + return; + } + + toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]); + } + }); + + // Fallback for the case where the error message is not in the compiled list + if ( + Array.from(uniqueErrorMessages).some( + (key) => !((key as string) in errMsgMap) + ) + ) { + type === 'bulk' && metadata.areMultipleUsersSelected + ? t('state.delete_users_failure') + : t('state.delete_user_failure'); + } + + return { data: type === 'bulk' ? { timeoutID } : null }; +}; diff --git a/packages/hoppscotch-sh-admin/src/modules/i18n.ts b/packages/hoppscotch-sh-admin/src/modules/i18n.ts index ac123b5016..a7cd885b11 100644 --- a/packages/hoppscotch-sh-admin/src/modules/i18n.ts +++ b/packages/hoppscotch-sh-admin/src/modules/i18n.ts @@ -1,7 +1,23 @@ -import { createI18n } from 'vue-i18n'; +import { I18n, createI18n } from 'vue-i18n'; import { HoppModule } from '.'; import messages from '@intlify/unplugin-vue-i18n/messages'; +// A reference to the i18n instance +let i18nInstance: I18n< + Record, + Record, + Record, + string, + false +> | null = null; + +/** + * Returns the i18n instance + */ +export function getI18n() { + return i18nInstance!.global.t; +} + export default { onVueAppInit(app) { const i18n = createI18n({ @@ -11,6 +27,9 @@ export default { legacy: false, allowComposition: true, }); + app.use(i18n); + + i18nInstance = i18n; }, }; diff --git a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue index a4f68ce184..1375ff6cdb 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue @@ -73,7 +73,7 @@ import { RemoveUsersByAdminDocument, UserInfoDocument, } from '~/helpers/backend/graphql'; -import { ADMIN_CANNOT_BE_DELETED, USER_IS_OWNER } from '~/helpers/errors'; +import { handleUserDeletion } from '~/helpers/userManagement'; const t = useI18n(); const toast = useToast(); @@ -210,35 +210,10 @@ const deleteUserMutation = async (id: string | null) => { } else { const deletedUsers = result.data?.removeUsersByAdmin || []; - const uniqueErrorMessages = new Set( - deletedUsers.map(({ errorMessage }) => errorMessage).filter(Boolean) - ) as Set; - - if (uniqueErrorMessages.size > 0) { - const errMsgMap = { - [ADMIN_CANNOT_BE_DELETED]: t('state.remove_admin_to_delete_user'), - [USER_IS_OWNER]: t('state.remove_owner_to_delete_user'), - }; - - const errMsgMapKeys = Object.keys(errMsgMap); - - uniqueErrorMessages.forEach((errorMessage) => { - if (errMsgMapKeys.includes(errorMessage)) { - toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]); - } - }); - - // Fallback for the case where the error message is not in the compiled list - if ( - Array.from(uniqueErrorMessages).some( - (key) => !((key as string) in errMsgMap) - ) - ) { - toast.error(t('state.delete_user_failure')); - } - } else { - toast.success(t('state.delete_user_success')); - } + handleUserDeletion(deletedUsers, { + type: 'individual', + metadata: null, + }); } confirmDeletion.value = false; deleteUserUID.value = null; diff --git a/packages/hoppscotch-sh-admin/src/pages/users/index.vue b/packages/hoppscotch-sh-admin/src/pages/users/index.vue index ac3f0a12c4..88aa9f5032 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/index.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/index.vue @@ -258,11 +258,8 @@ import { UsersListQuery, UsersListV2Document, } from '~/helpers/backend/graphql'; -import { - ADMIN_CANNOT_BE_DELETED, - USER_ALREADY_INVITED, - USER_IS_OWNER, -} from '~/helpers/errors'; +import { USER_ALREADY_INVITED } from '~/helpers/errors'; +import { handleUserDeletion } from '~/helpers/userManagement'; import IconCheck from '~icons/lucide/check'; import IconLeft from '~icons/lucide/chevron-left'; import IconRight from '~icons/lucide/chevron-right'; @@ -586,57 +583,16 @@ const deleteUsers = async (id: string | null) => { .filter((user) => user.isDeleted) .map((user) => user.userUID); - const uniqueErrorMessages = new Set( - deletedUsers.map(({ errorMessage }) => errorMessage).filter(Boolean) - ) as Set; - - if (uniqueErrorMessages.size > 0) { - const errMsgMap = { - [ADMIN_CANNOT_BE_DELETED]: t('state.remove_admin_for_deletion'), - [USER_IS_OWNER]: t('state.remove_owner_for_deletion'), - }; - - const errMsgMapKeys = Object.keys(errMsgMap); - - // Show toast messages with the count of users deleted only if multiple users are selected - if (areMultipleUsersSelected.value) { - toast.success( - t('state.delete_some_users_success', { count: deletedIDs.length }) - ); - toast.error( - t('state.delete_some_users_failure', { - count: deletedUsers.length - deletedIDs.length, - }) - ); - } - - uniqueErrorMessages.forEach((errorMessage) => { - if (errMsgMapKeys.includes(errorMessage)) { - toastTimeout = setTimeout( - () => { - toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]); - }, - areMultipleUsersSelected.value ? 2000 : 0 - ); - } - }); - - // Fallback for the case where the error message is not in the compiled list - if ( - Array.from(uniqueErrorMessages).some( - (key) => !((key as string) in errMsgMap) - ) - ) { - areMultipleUsersSelected.value - ? t('state.delete_users_failure') - : t('state.delete_user_failure'); - } - } else { - toast.success( - areMultipleUsersSelected.value - ? t('state.delete_users_success') - : t('state.delete_user_success') - ); + const { data } = handleUserDeletion(deletedUsers, { + type: 'bulk', + metadata: { + areMultipleUsersSelected: areMultipleUsersSelected.value, + deletedIDs, + }, + }); + + if (data?.timeoutID) { + toastTimeout = data.timeoutID; } usersList.value = usersList.value.filter( From 8a8cdcf78b2f0ba8905eb1111d3f626b6ea9c405 Mon Sep 17 00:00:00 2001 From: jamesgeorge007 Date: Wed, 27 Mar 2024 14:45:19 +0530 Subject: [PATCH 3/6] chore: more specific error message while removing Admin status Action leading to a scenario where there are no users with Admin privileges. --- packages/hoppscotch-sh-admin/locales/en.json | 1 + packages/hoppscotch-sh-admin/src/helpers/errors.ts | 4 ++-- packages/hoppscotch-sh-admin/src/pages/users/index.vue | 9 ++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/hoppscotch-sh-admin/locales/en.json b/packages/hoppscotch-sh-admin/locales/en.json index 0427a80cb2..cb06e5e591 100644 --- a/packages/hoppscotch-sh-admin/locales/en.json +++ b/packages/hoppscotch-sh-admin/locales/en.json @@ -164,6 +164,7 @@ "privacy_policy": "Privacy Policy", "reenter_email": "Re-enter email", "remove_admin_failure": "Failed to remove admin status!!", + "remove_admin_failure_only_one_admin": "Failed to remove admin status. There should be at least one admin!!", "remove_admin_success": "Admin status removed!!", "remove_admin_from_users_failure": "Failed to remove admin status from selected users!!", "remove_admin_from_users_success": "Admin status removed from selected users!!", diff --git a/packages/hoppscotch-sh-admin/src/helpers/errors.ts b/packages/hoppscotch-sh-admin/src/helpers/errors.ts index d1ba9bdfe0..fb7f8f2a3e 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/errors.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/errors.ts @@ -8,8 +8,8 @@ export const UNAUTHORIZED = 'Unauthorized' as const; // Sometimes the backend returns Unauthorized error message as follows: export const GRAPHQL_UNAUTHORIZED = '[GraphQL] Unauthorized' as const; -export const DELETE_USER_FAILED_ONLY_ONE_ADMIN = - 'admin/only_one_admin_account_found' as const; +export const ONLY_ONE_ADMIN_ACCOUNT_FOUND = + '[GraphQL] admin/only_one_admin_account_found' as const; export const ADMIN_CANNOT_BE_DELETED = 'admin/admin_can_not_be_deleted' as const; diff --git a/packages/hoppscotch-sh-admin/src/pages/users/index.vue b/packages/hoppscotch-sh-admin/src/pages/users/index.vue index 88aa9f5032..1db77d937b 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/index.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/index.vue @@ -258,7 +258,10 @@ import { UsersListQuery, UsersListV2Document, } from '~/helpers/backend/graphql'; -import { USER_ALREADY_INVITED } from '~/helpers/errors'; +import { + ONLY_ONE_ADMIN_ACCOUNT_FOUND, + USER_ALREADY_INVITED, +} from '~/helpers/errors'; import { handleUserDeletion } from '~/helpers/userManagement'; import IconCheck from '~icons/lucide/check'; import IconLeft from '~icons/lucide/chevron-left'; @@ -525,6 +528,10 @@ const makeAdminsToUsers = async (id: string | null) => { const variables = { userUIDs }; const result = await adminsToUser.executeMutation(variables); if (result.error) { + if (result.error.message === ONLY_ONE_ADMIN_ACCOUNT_FOUND) { + return toast.error(t('state.remove_admin_failure_only_one_admin')); + } + toast.error( areMultipleUsersSelected.value ? t('state.remove_admin_from_users_failure') From dee7864a080ac89afd9fad701c48f5af8618b557 Mon Sep 17 00:00:00 2001 From: jamesgeorge007 Date: Wed, 27 Mar 2024 17:42:20 +0530 Subject: [PATCH 4/6] chore: address CR comments - Implicitly infer the action type (bulk/individual) from the supplied deleted users list. - Display toast messages one after the other by relying on the native toast APIs refraining from the need to maintain timeouts separately. - Ensure the toast message about user deletion success/failure with the count is displayed only when above `0`. - Cleanup. Co-authored-by: amk-dev Co-authored-by: nivedin --- .../src/helpers/userManagement.ts | 142 ++++++++++-------- .../src/pages/users/_id.vue | 5 +- .../src/pages/users/index.vue | 23 +-- 3 files changed, 83 insertions(+), 87 deletions(-) diff --git a/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts b/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts index 662c32cd44..21f700084c 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts @@ -3,106 +3,112 @@ import { getI18n } from '~/modules/i18n'; import { UserDeletionResult } from './backend/graphql'; import { ADMIN_CANNOT_BE_DELETED, USER_IS_OWNER } from './errors'; -type IndividualActionInput = { - type: 'individual'; - metadata: null; -}; -type BulkActionInput = { - type: 'bulk'; - metadata: { - areMultipleUsersSelected: boolean; - deletedIDs: string[]; - }; +type IndividualActionMetadata = null; +type BulkActionMetadata = { + areMultipleUsersSelected: boolean; + deletedUserIDs: string[]; }; +type ActionMetadata = IndividualActionMetadata | BulkActionMetadata; -type IndividualActionResult = { - data: null; -}; -type BulkActionResult = { - data: { timeoutID: NodeJS.Timeout | null }; +type HandleUserDeletion = { + (deletedUsersList: UserDeletionResult[], metadata: ActionMetadata): void; }; -type HandleUserDeletion = { - ( - deletedUsersList: UserDeletionResult[], - action: IndividualActionInput | BulkActionInput - ): IndividualActionResult | BulkActionResult; +type ToastMessage = { + message: string; + state: 'success' | 'error'; }; const t = getI18n(); const toast = useToast(); +const displayToastMessages = ( + toastMessages: ToastMessage[], + currentIndex: number +) => { + const { message, state } = toastMessages[currentIndex]; + + toast[state](message, { + duration: 2000, + onComplete: () => { + if (currentIndex < toastMessages.length - 1) { + displayToastMessages(toastMessages, currentIndex + 1); + } + }, + }); +}; + export const handleUserDeletion: HandleUserDeletion = ( deletedUsersList, - action + metadata ) => { - let timeoutID: NodeJS.Timeout | null = null; - const uniqueErrorMessages = new Set( deletedUsersList.map(({ errorMessage }) => errorMessage).filter(Boolean) ) as Set; - const { type, metadata } = action; + const isBulkAction = deletedUsersList.length > 1; // Show the success toast based on the action type if there are no errors if (uniqueErrorMessages.size === 0) { - if (type === 'bulk') { + if (isBulkAction) { toast.success( - metadata.areMultipleUsersSelected + (metadata as BulkActionMetadata).areMultipleUsersSelected ? t('state.delete_user_success') : t('state.delete_users_success') ); - return { type, data: { timeoutID } }; + return; } toast.success(t('state.delete_user_success')); - return { type, data: null }; + return; } const errMsgMap = { - [ADMIN_CANNOT_BE_DELETED]: - type === 'bulk' - ? t('state.remove_admin_for_deletion') - : t('state.remove_admin_to_delete_user'), - - [USER_IS_OWNER]: - type === 'bulk' - ? t('state.remove_owner_for_deletion') - : t('state.remove_owner_to_delete_user'), + [ADMIN_CANNOT_BE_DELETED]: isBulkAction + ? t('state.remove_admin_for_deletion') + : t('state.remove_admin_to_delete_user'), + + [USER_IS_OWNER]: isBulkAction + ? t('state.remove_owner_for_deletion') + : t('state.remove_owner_to_delete_user'), }; const errMsgMapKeys = Object.keys(errMsgMap); - if (type === 'bulk') { - const { areMultipleUsersSelected, deletedIDs } = metadata; + const toastMessages: ToastMessage[] = []; - // Show toast messages with the count of users deleted only if multiple users are selected - if (areMultipleUsersSelected) { - toast.success( - t('state.delete_some_users_success', { count: deletedIDs.length }) - ); - toast.error( - t('state.delete_some_users_failure', { - count: deletedUsersList.length - deletedIDs.length, - }) - ); + if (isBulkAction) { + const { areMultipleUsersSelected, deletedUserIDs } = + metadata as BulkActionMetadata; + + // Indicates the actual count of users deleted (filtered via the `isDeleted` field) + const deletedUsersCount = deletedUserIDs.length; + + if (areMultipleUsersSelected && deletedUsersCount > 0) { + toastMessages.push({ + message: t('state.delete_some_users_success', { + count: deletedUsersCount, + }), + state: 'success', + }); + } + const remainingDeletionsCount = deletedUsersList.length - deletedUsersCount; + if (remainingDeletionsCount > 0) { + toastMessages.push({ + message: t('state.delete_some_users_failure', { + count: remainingDeletionsCount, + }), + state: 'error', + }); } } uniqueErrorMessages.forEach((errorMessage) => { if (errMsgMapKeys.includes(errorMessage)) { - if (type === 'bulk') { - timeoutID = setTimeout( - () => { - toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]); - }, - metadata.areMultipleUsersSelected ? 2000 : 0 - ); - - return; - } - - toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]); + toastMessages.push({ + message: errMsgMap[errorMessage as keyof typeof errMsgMap], + state: 'error', + }); } }); @@ -112,10 +118,16 @@ export const handleUserDeletion: HandleUserDeletion = ( (key) => !((key as string) in errMsgMap) ) ) { - type === 'bulk' && metadata.areMultipleUsersSelected - ? t('state.delete_users_failure') - : t('state.delete_user_failure'); + const fallbackErrMsg = + isBulkAction && (metadata as BulkActionMetadata).areMultipleUsersSelected + ? t('state.delete_users_failure') + : t('state.delete_user_failure'); + + toastMessages.push({ + message: fallbackErrMsg, + state: 'error', + }); } - return { data: type === 'bulk' ? { timeoutID } : null }; + displayToastMessages(toastMessages, 0); }; diff --git a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue index 1375ff6cdb..6151b766f3 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue @@ -210,10 +210,7 @@ const deleteUserMutation = async (id: string | null) => { } else { const deletedUsers = result.data?.removeUsersByAdmin || []; - handleUserDeletion(deletedUsers, { - type: 'individual', - metadata: null, - }); + handleUserDeletion(deletedUsers, null); } confirmDeletion.value = false; deleteUserUID.value = null; diff --git a/packages/hoppscotch-sh-admin/src/pages/users/index.vue b/packages/hoppscotch-sh-admin/src/pages/users/index.vue index 1db77d937b..7a49b2e37b 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/index.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/index.vue @@ -309,16 +309,10 @@ const selectedRows = ref([]); // Ensure this variable is declared outside the debounce function let debounceTimeout: ReturnType | null = null; -let toastTimeout: ReturnType | null = null; - onUnmounted(() => { if (debounceTimeout) { clearTimeout(debounceTimeout); } - - if (toastTimeout) { - clearTimeout(toastTimeout); - } }); // Debounce Function @@ -586,24 +580,17 @@ const deleteUsers = async (id: string | null) => { toast.error(errorMessage); } else { const deletedUsers = result.data?.removeUsersByAdmin || []; - const deletedIDs = deletedUsers + const deletedUserIDs = deletedUsers .filter((user) => user.isDeleted) .map((user) => user.userUID); - const { data } = handleUserDeletion(deletedUsers, { - type: 'bulk', - metadata: { - areMultipleUsersSelected: areMultipleUsersSelected.value, - deletedIDs, - }, + handleUserDeletion(deletedUsers, { + areMultipleUsersSelected: areMultipleUsersSelected.value, + deletedUserIDs, }); - if (data?.timeoutID) { - toastTimeout = data.timeoutID; - } - usersList.value = usersList.value.filter( - (user) => !deletedIDs.includes(user.uid) + (user) => !deletedUserIDs.includes(user.uid) ); selectedRows.value.splice(0, selectedRows.value.length); From 986a4b1d549c1662bcff8fefc9433859f53ed2ea Mon Sep 17 00:00:00 2001 From: amk-dev Date: Thu, 28 Mar 2024 17:25:10 +0530 Subject: [PATCH 5/6] refactor: remove metadata + simplify types --- .../src/helpers/userManagement.ts | 34 ++++++------------- .../src/pages/users/_id.vue | 2 +- .../src/pages/users/index.vue | 5 +-- 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts b/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts index 21f700084c..605602cb7c 100644 --- a/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts +++ b/packages/hoppscotch-sh-admin/src/helpers/userManagement.ts @@ -3,17 +3,6 @@ import { getI18n } from '~/modules/i18n'; import { UserDeletionResult } from './backend/graphql'; import { ADMIN_CANNOT_BE_DELETED, USER_IS_OWNER } from './errors'; -type IndividualActionMetadata = null; -type BulkActionMetadata = { - areMultipleUsersSelected: boolean; - deletedUserIDs: string[]; -}; -type ActionMetadata = IndividualActionMetadata | BulkActionMetadata; - -type HandleUserDeletion = { - (deletedUsersList: UserDeletionResult[], metadata: ActionMetadata): void; -}; - type ToastMessage = { message: string; state: 'success' | 'error'; @@ -38,21 +27,22 @@ const displayToastMessages = ( }); }; -export const handleUserDeletion: HandleUserDeletion = ( - deletedUsersList, - metadata -) => { +export const handleUserDeletion = (deletedUsersList: UserDeletionResult[]) => { const uniqueErrorMessages = new Set( deletedUsersList.map(({ errorMessage }) => errorMessage).filter(Boolean) ) as Set; const isBulkAction = deletedUsersList.length > 1; + const deletedUserIDs = deletedUsersList + .filter((user) => user.isDeleted) + .map((user) => user.userUID); + // Show the success toast based on the action type if there are no errors if (uniqueErrorMessages.size === 0) { if (isBulkAction) { toast.success( - (metadata as BulkActionMetadata).areMultipleUsersSelected + isBulkAction ? t('state.delete_user_success') : t('state.delete_users_success') ); @@ -78,13 +68,10 @@ export const handleUserDeletion: HandleUserDeletion = ( const toastMessages: ToastMessage[] = []; if (isBulkAction) { - const { areMultipleUsersSelected, deletedUserIDs } = - metadata as BulkActionMetadata; - // Indicates the actual count of users deleted (filtered via the `isDeleted` field) const deletedUsersCount = deletedUserIDs.length; - if (areMultipleUsersSelected && deletedUsersCount > 0) { + if (isBulkAction && deletedUsersCount > 0) { toastMessages.push({ message: t('state.delete_some_users_success', { count: deletedUsersCount, @@ -118,10 +105,9 @@ export const handleUserDeletion: HandleUserDeletion = ( (key) => !((key as string) in errMsgMap) ) ) { - const fallbackErrMsg = - isBulkAction && (metadata as BulkActionMetadata).areMultipleUsersSelected - ? t('state.delete_users_failure') - : t('state.delete_user_failure'); + const fallbackErrMsg = isBulkAction + ? t('state.delete_users_failure') + : t('state.delete_user_failure'); toastMessages.push({ message: fallbackErrMsg, diff --git a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue index 6151b766f3..b7e04e1c89 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue @@ -210,7 +210,7 @@ const deleteUserMutation = async (id: string | null) => { } else { const deletedUsers = result.data?.removeUsersByAdmin || []; - handleUserDeletion(deletedUsers, null); + handleUserDeletion(deletedUsers); } confirmDeletion.value = false; deleteUserUID.value = null; diff --git a/packages/hoppscotch-sh-admin/src/pages/users/index.vue b/packages/hoppscotch-sh-admin/src/pages/users/index.vue index 7a49b2e37b..ad2079105d 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/index.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/index.vue @@ -584,10 +584,7 @@ const deleteUsers = async (id: string | null) => { .filter((user) => user.isDeleted) .map((user) => user.userUID); - handleUserDeletion(deletedUsers, { - areMultipleUsersSelected: areMultipleUsersSelected.value, - deletedUserIDs, - }); + handleUserDeletion(deletedUsers); usersList.value = usersList.value.filter( (user) => !deletedUserIDs.includes(user.uid) From d6babae291e9b17ced1e3d0d40ee4eb21ceebf05 Mon Sep 17 00:00:00 2001 From: nivedin Date: Thu, 28 Mar 2024 19:48:32 +0530 Subject: [PATCH 6/6] chore: redirect to users page only in error case after user deletion --- packages/hoppscotch-sh-admin/src/pages/users/_id.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue index b7e04e1c89..05b22059d4 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue @@ -207,6 +207,7 @@ const deleteUserMutation = async (id: string | null) => { if (result.error) { toast.error(t('state.delete_user_failure')); + router.push('/users'); } else { const deletedUsers = result.data?.removeUsersByAdmin || []; @@ -214,6 +215,5 @@ const deleteUserMutation = async (id: string | null) => { } confirmDeletion.value = false; deleteUserUID.value = null; - router.push('/users'); };