Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(sh-admin): alert the user while deleting users who are team owners #3937

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/hoppscotch-sh-admin/locales/en.json
Expand Up @@ -164,11 +164,14 @@
"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!!",
"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!!",
Expand Down
7 changes: 5 additions & 2 deletions packages/hoppscotch-sh-admin/src/helpers/errors.ts
Expand Up @@ -8,12 +8,15 @@ 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;

// 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;
121 changes: 121 additions & 0 deletions 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[];
};
};
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved

type IndividualActionResult = {
data: null;
};
type BulkActionResult = {
data: { timeoutID: NodeJS.Timeout | null };
};
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved

type HandleUserDeletion = {
(
deletedUsersList: UserDeletionResult[],
action: IndividualActionInput | BulkActionInput
): IndividualActionResult | BulkActionResult;
};

const t = getI18n();
const toast = useToast();

export const handleUserDeletion: HandleUserDeletion = (
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved
deletedUsersList,
action
) => {
let timeoutID: NodeJS.Timeout | null = null;

const uniqueErrorMessages = new Set(
deletedUsersList.map(({ errorMessage }) => errorMessage).filter(Boolean)
) as Set<string>;
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved

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 };
}
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved

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'),
};
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved
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,
})
);
}
}
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved

uniqueErrorMessages.forEach((errorMessage) => {
if (errMsgMapKeys.includes(errorMessage)) {
if (type === 'bulk') {
timeoutID = setTimeout(
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved
() => {
toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]);
},
metadata.areMultipleUsersSelected ? 2000 : 0
);

return;
}

toast.error(errMsgMap[errorMessage as keyof typeof errMsgMap]);
}
});
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved

// 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 };
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved
};
21 changes: 20 additions & 1 deletion 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<string, unknown>,
Record<string, unknown>,
Record<string, unknown>,
string,
false
> | null = null;

/**
* Returns the i18n instance
*/
export function getI18n() {
return i18nInstance!.global.t;
}

export default <HoppModule>{
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved
onVueAppInit(app) {
const i18n = createI18n({
Expand All @@ -11,6 +27,9 @@ export default <HoppModule>{
legacy: false,
allowComposition: true,
});

app.use(i18n);

i18nInstance = i18n;
},
};
13 changes: 5 additions & 8 deletions packages/hoppscotch-sh-admin/src/pages/users/_id.vue
Expand Up @@ -73,7 +73,7 @@ import {
RemoveUsersByAdminDocument,
UserInfoDocument,
} from '~/helpers/backend/graphql';
import { ADMIN_CANNOT_BE_DELETED } from '~/helpers/errors';
import { handleUserDeletion } from '~/helpers/userManagement';

const t = useI18n();
const toast = useToast();
Expand Down Expand Up @@ -210,13 +210,10 @@ 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'));
handleUserDeletion(deletedUsers, {
type: 'individual',
metadata: null,
});
}
confirmDeletion.value = false;
deleteUserUID.value = null;
Expand Down
81 changes: 38 additions & 43 deletions packages/hoppscotch-sh-admin/src/pages/users/index.vue
Expand Up @@ -210,7 +210,7 @@
<HoppSmartConfirmModal
:show="confirmUsersToAdmin"
:title="
AreMultipleUsersSelected
areMultipleUsersSelected
? t('state.confirm_users_to_admin')
: t('state.confirm_user_to_admin')
"
Expand All @@ -220,7 +220,7 @@
<HoppSmartConfirmModal
:show="confirmAdminsToUsers"
:title="
AreMultipleUsersSelectedToAdmin
areMultipleUsersSelectedToAdmin
? t('state.confirm_admins_to_users')
: t('state.confirm_admin_to_user')
"
Expand All @@ -230,7 +230,7 @@
<HoppSmartConfirmModal
:show="confirmUsersDeletion"
:title="
AreMultipleUsersSelectedForDeletion
areMultipleUsersSelectedForDeletion
? t('state.confirm_users_deletion')
: t('state.confirm_user_deletion')
"
Expand Down Expand Up @@ -259,10 +259,10 @@ import {
UsersListV2Document,
} from '~/helpers/backend/graphql';
import {
ADMIN_CANNOT_BE_DELETED,
DELETE_USER_FAILED_ONLY_ONE_ADMIN,
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';
import IconRight from '~icons/lucide/chevron-right';
Expand Down Expand Up @@ -462,7 +462,7 @@ const confirmUsersToAdmin = ref(false);
const usersToAdminUID = ref<string | null>(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;
Expand All @@ -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,
Expand Down Expand Up @@ -514,7 +518,7 @@ const resetConfirmAdminToUser = () => {
adminsToUserUID.value = null;
};

const AreMultipleUsersSelectedToAdmin = computed(
const areMultipleUsersSelectedToAdmin = computed(
() => selectedRows.value.length > 1
);

Expand All @@ -524,16 +528,20 @@ 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(
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,
Expand Down Expand Up @@ -562,7 +570,7 @@ const resetConfirmUserDeletion = () => {
deleteUserUID.value = null;
};

const AreMultipleUsersSelectedForDeletion = computed(
const areMultipleUsersSelectedForDeletion = computed(
() => selectedRows.value.length > 1
);

Expand All @@ -572,45 +580,32 @@ 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')
jamesgeorge007 marked this conversation as resolved.
Show resolved Hide resolved
: 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 || [];
const deletedIDs = deletedUsers
.filter((user) => user.isDeleted)
.map((user) => user.userUID);

const isAdminError = deletedUsers.some(
(user) => user.errorMessage === ADMIN_CANNOT_BE_DELETED
);
const { data } = handleUserDeletion(deletedUsers, {
type: 'bulk',
metadata: {
areMultipleUsersSelected: areMultipleUsersSelected.value,
deletedIDs,
},
});

if (data?.timeoutID) {
toastTimeout = data.timeoutID;
}

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);
} else {
toast.success(
id ? t('state.delete_user_success') : t('state.delete_users_success')
);
}

selectedRows.value.splice(0, selectedRows.value.length);
}
confirmUsersDeletion.value = false;
Expand Down