Compare commits
6 Commits
fix/defaul
...
pr/jamesge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6babae291 | ||
|
|
986a4b1d54 | ||
|
|
dee7864a08 | ||
|
|
8a8cdcf78b | ||
|
|
17db483a35 | ||
|
|
80b9941399 |
@@ -164,11 +164,14 @@
|
|||||||
"privacy_policy": "Privacy Policy",
|
"privacy_policy": "Privacy Policy",
|
||||||
"reenter_email": "Re-enter email",
|
"reenter_email": "Re-enter email",
|
||||||
"remove_admin_failure": "Failed to remove admin status!!",
|
"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_success": "Admin status removed!!",
|
||||||
"remove_admin_from_users_failure": "Failed to remove admin status from selected users!!",
|
"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_from_users_success": "Admin status removed from selected users!!",
|
||||||
"remove_admin_to_delete_user": "Remove admin privilege to delete the user!!",
|
"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_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_failure": "Removal of invitee failed!!",
|
||||||
"remove_invitee_success": "Removal of invitee is successfull!!",
|
"remove_invitee_success": "Removal of invitee is successfull!!",
|
||||||
"remove_member_failure": "Member couldn't be removed!!",
|
"remove_member_failure": "Member couldn't be removed!!",
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ export const UNAUTHORIZED = 'Unauthorized' as const;
|
|||||||
// Sometimes the backend returns Unauthorized error message as follows:
|
// Sometimes the backend returns Unauthorized error message as follows:
|
||||||
export const GRAPHQL_UNAUTHORIZED = '[GraphQL] Unauthorized' as const;
|
export const GRAPHQL_UNAUTHORIZED = '[GraphQL] Unauthorized' as const;
|
||||||
|
|
||||||
export const DELETE_USER_FAILED_ONLY_ONE_ADMIN =
|
export const ONLY_ONE_ADMIN_ACCOUNT_FOUND =
|
||||||
'admin/only_one_admin_account_found' as const;
|
'[GraphQL] admin/only_one_admin_account_found' as const;
|
||||||
|
|
||||||
export const ADMIN_CANNOT_BE_DELETED =
|
export const ADMIN_CANNOT_BE_DELETED =
|
||||||
'admin/admin_can_not_be_deleted' as const;
|
'admin/admin_can_not_be_deleted' as const;
|
||||||
@@ -17,3 +17,6 @@ export const ADMIN_CANNOT_BE_DELETED =
|
|||||||
// When trying to invite a user that is already invited
|
// When trying to invite a user that is already invited
|
||||||
export const USER_ALREADY_INVITED =
|
export const USER_ALREADY_INVITED =
|
||||||
'[GraphQL] admin/user_already_invited' as const;
|
'[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;
|
||||||
|
|||||||
119
packages/hoppscotch-sh-admin/src/helpers/userManagement.ts
Normal file
119
packages/hoppscotch-sh-admin/src/helpers/userManagement.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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 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 = (deletedUsersList: UserDeletionResult[]) => {
|
||||||
|
const uniqueErrorMessages = new Set(
|
||||||
|
deletedUsersList.map(({ errorMessage }) => errorMessage).filter(Boolean)
|
||||||
|
) as Set<string>;
|
||||||
|
|
||||||
|
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(
|
||||||
|
isBulkAction
|
||||||
|
? t('state.delete_user_success')
|
||||||
|
: t('state.delete_users_success')
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(t('state.delete_user_success'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errMsgMap = {
|
||||||
|
[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);
|
||||||
|
|
||||||
|
const toastMessages: ToastMessage[] = [];
|
||||||
|
|
||||||
|
if (isBulkAction) {
|
||||||
|
// Indicates the actual count of users deleted (filtered via the `isDeleted` field)
|
||||||
|
const deletedUsersCount = deletedUserIDs.length;
|
||||||
|
|
||||||
|
if (isBulkAction && 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)) {
|
||||||
|
toastMessages.push({
|
||||||
|
message: errMsgMap[errorMessage as keyof typeof errMsgMap],
|
||||||
|
state: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const fallbackErrMsg = isBulkAction
|
||||||
|
? t('state.delete_users_failure')
|
||||||
|
: t('state.delete_user_failure');
|
||||||
|
|
||||||
|
toastMessages.push({
|
||||||
|
message: fallbackErrMsg,
|
||||||
|
state: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
displayToastMessages(toastMessages, 0);
|
||||||
|
};
|
||||||
@@ -1,7 +1,23 @@
|
|||||||
import { createI18n } from 'vue-i18n';
|
import { I18n, createI18n } from 'vue-i18n';
|
||||||
import { HoppModule } from '.';
|
import { HoppModule } from '.';
|
||||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
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>{
|
export default <HoppModule>{
|
||||||
onVueAppInit(app) {
|
onVueAppInit(app) {
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
@@ -11,6 +27,9 @@ export default <HoppModule>{
|
|||||||
legacy: false,
|
legacy: false,
|
||||||
allowComposition: true,
|
allowComposition: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
|
|
||||||
|
i18nInstance = i18n;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ import {
|
|||||||
RemoveUsersByAdminDocument,
|
RemoveUsersByAdminDocument,
|
||||||
UserInfoDocument,
|
UserInfoDocument,
|
||||||
} from '~/helpers/backend/graphql';
|
} from '~/helpers/backend/graphql';
|
||||||
import { ADMIN_CANNOT_BE_DELETED } from '~/helpers/errors';
|
import { handleUserDeletion } from '~/helpers/userManagement';
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -207,19 +207,13 @@ const deleteUserMutation = async (id: string | null) => {
|
|||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast.error(t('state.delete_user_failure'));
|
toast.error(t('state.delete_user_failure'));
|
||||||
|
router.push('/users');
|
||||||
} else {
|
} else {
|
||||||
const deletedUsers = result.data?.removeUsersByAdmin || [];
|
const deletedUsers = result.data?.removeUsersByAdmin || [];
|
||||||
|
|
||||||
const isAdminError = deletedUsers.some(
|
handleUserDeletion(deletedUsers);
|
||||||
(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'));
|
|
||||||
}
|
}
|
||||||
confirmDeletion.value = false;
|
confirmDeletion.value = false;
|
||||||
deleteUserUID.value = null;
|
deleteUserUID.value = null;
|
||||||
router.push('/users');
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -210,7 +210,7 @@
|
|||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmUsersToAdmin"
|
:show="confirmUsersToAdmin"
|
||||||
:title="
|
:title="
|
||||||
AreMultipleUsersSelected
|
areMultipleUsersSelected
|
||||||
? t('state.confirm_users_to_admin')
|
? t('state.confirm_users_to_admin')
|
||||||
: t('state.confirm_user_to_admin')
|
: t('state.confirm_user_to_admin')
|
||||||
"
|
"
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmAdminsToUsers"
|
:show="confirmAdminsToUsers"
|
||||||
:title="
|
:title="
|
||||||
AreMultipleUsersSelectedToAdmin
|
areMultipleUsersSelectedToAdmin
|
||||||
? t('state.confirm_admins_to_users')
|
? t('state.confirm_admins_to_users')
|
||||||
: t('state.confirm_admin_to_user')
|
: t('state.confirm_admin_to_user')
|
||||||
"
|
"
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmUsersDeletion"
|
:show="confirmUsersDeletion"
|
||||||
:title="
|
:title="
|
||||||
AreMultipleUsersSelectedForDeletion
|
areMultipleUsersSelectedForDeletion
|
||||||
? t('state.confirm_users_deletion')
|
? t('state.confirm_users_deletion')
|
||||||
: t('state.confirm_user_deletion')
|
: t('state.confirm_user_deletion')
|
||||||
"
|
"
|
||||||
@@ -259,10 +259,10 @@ import {
|
|||||||
UsersListV2Document,
|
UsersListV2Document,
|
||||||
} from '~/helpers/backend/graphql';
|
} from '~/helpers/backend/graphql';
|
||||||
import {
|
import {
|
||||||
ADMIN_CANNOT_BE_DELETED,
|
ONLY_ONE_ADMIN_ACCOUNT_FOUND,
|
||||||
DELETE_USER_FAILED_ONLY_ONE_ADMIN,
|
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
} from '~/helpers/errors';
|
} from '~/helpers/errors';
|
||||||
|
import { handleUserDeletion } from '~/helpers/userManagement';
|
||||||
import IconCheck from '~icons/lucide/check';
|
import IconCheck from '~icons/lucide/check';
|
||||||
import IconLeft from '~icons/lucide/chevron-left';
|
import IconLeft from '~icons/lucide/chevron-left';
|
||||||
import IconRight from '~icons/lucide/chevron-right';
|
import IconRight from '~icons/lucide/chevron-right';
|
||||||
@@ -309,16 +309,10 @@ const selectedRows = ref<UsersListQuery['infra']['allUsers']>([]);
|
|||||||
// Ensure this variable is declared outside the debounce function
|
// Ensure this variable is declared outside the debounce function
|
||||||
let debounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
let debounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
let toastTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (debounceTimeout) {
|
if (debounceTimeout) {
|
||||||
clearTimeout(debounceTimeout);
|
clearTimeout(debounceTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toastTimeout) {
|
|
||||||
clearTimeout(toastTimeout);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debounce Function
|
// Debounce Function
|
||||||
@@ -462,7 +456,7 @@ const confirmUsersToAdmin = ref(false);
|
|||||||
const usersToAdminUID = ref<string | null>(null);
|
const usersToAdminUID = ref<string | null>(null);
|
||||||
const usersToAdmin = useMutation(MakeUsersAdminDocument);
|
const usersToAdmin = useMutation(MakeUsersAdminDocument);
|
||||||
|
|
||||||
const AreMultipleUsersSelected = computed(() => selectedRows.value.length > 1);
|
const areMultipleUsersSelected = computed(() => selectedRows.value.length > 1);
|
||||||
|
|
||||||
const confirmUserToAdmin = (id: string | null) => {
|
const confirmUserToAdmin = (id: string | null) => {
|
||||||
confirmUsersToAdmin.value = true;
|
confirmUsersToAdmin.value = true;
|
||||||
@@ -482,11 +476,15 @@ const makeUsersToAdmin = async (id: string | null) => {
|
|||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast.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 {
|
} else {
|
||||||
toast.success(
|
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) => ({
|
usersList.value = usersList.value.map((user) => ({
|
||||||
...user,
|
...user,
|
||||||
@@ -514,7 +512,7 @@ const resetConfirmAdminToUser = () => {
|
|||||||
adminsToUserUID.value = null;
|
adminsToUserUID.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AreMultipleUsersSelectedToAdmin = computed(
|
const areMultipleUsersSelectedToAdmin = computed(
|
||||||
() => selectedRows.value.length > 1
|
() => selectedRows.value.length > 1
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -524,16 +522,20 @@ const makeAdminsToUsers = async (id: string | null) => {
|
|||||||
const variables = { userUIDs };
|
const variables = { userUIDs };
|
||||||
const result = await adminsToUser.executeMutation(variables);
|
const result = await adminsToUser.executeMutation(variables);
|
||||||
if (result.error) {
|
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(
|
toast.error(
|
||||||
id
|
areMultipleUsersSelected.value
|
||||||
? t('state.remove_admin_failure')
|
? t('state.remove_admin_from_users_failure')
|
||||||
: t('state.remove_admin_from_users_failure')
|
: t('state.remove_admin_failure')
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.success(
|
toast.success(
|
||||||
id
|
areMultipleUsersSelected.value
|
||||||
? t('state.remove_admin_success')
|
? t('state.remove_admin_from_users_success')
|
||||||
: t('state.remove_admin_from_users_success')
|
: t('state.remove_admin_success')
|
||||||
);
|
);
|
||||||
usersList.value = usersList.value.map((user) => ({
|
usersList.value = usersList.value.map((user) => ({
|
||||||
...user,
|
...user,
|
||||||
@@ -562,7 +564,7 @@ const resetConfirmUserDeletion = () => {
|
|||||||
deleteUserUID.value = null;
|
deleteUserUID.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AreMultipleUsersSelectedForDeletion = computed(
|
const areMultipleUsersSelectedForDeletion = computed(
|
||||||
() => selectedRows.value.length > 1
|
() => selectedRows.value.length > 1
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -572,45 +574,22 @@ const deleteUsers = async (id: string | null) => {
|
|||||||
const result = await usersDeletion.executeMutation(variables);
|
const result = await usersDeletion.executeMutation(variables);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
const errorMessage =
|
const errorMessage = areMultipleUsersSelected.value
|
||||||
result.error.message === DELETE_USER_FAILED_ONLY_ONE_ADMIN
|
? t('state.delete_users_failure')
|
||||||
? t('state.delete_user_failed_only_one_admin')
|
: t('state.delete_user_failure');
|
||||||
: id
|
|
||||||
? t('state.delete_user_failure')
|
|
||||||
: t('state.delete_users_failure');
|
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
} else {
|
} else {
|
||||||
const deletedUsers = result.data?.removeUsersByAdmin || [];
|
const deletedUsers = result.data?.removeUsersByAdmin || [];
|
||||||
const deletedIDs = deletedUsers
|
const deletedUserIDs = deletedUsers
|
||||||
.filter((user) => user.isDeleted)
|
.filter((user) => user.isDeleted)
|
||||||
.map((user) => user.userUID);
|
.map((user) => user.userUID);
|
||||||
|
|
||||||
const isAdminError = deletedUsers.some(
|
handleUserDeletion(deletedUsers);
|
||||||
(user) => user.errorMessage === ADMIN_CANNOT_BE_DELETED
|
|
||||||
);
|
|
||||||
|
|
||||||
usersList.value = usersList.value.filter(
|
usersList.value = usersList.value.filter(
|
||||||
(user) => !deletedIDs.includes(user.uid)
|
(user) => !deletedUserIDs.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);
|
selectedRows.value.splice(0, selectedRows.value.length);
|
||||||
}
|
}
|
||||||
confirmUsersDeletion.value = false;
|
confirmUsersDeletion.value = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user