feat(sh-admin): enhanced user management in admin dashboard (#3814)

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
Joel Jacob Stephen
2024-03-13 14:45:13 +05:30
committed by GitHub
parent 8fdba760a2
commit acfb0189df
23 changed files with 2698 additions and 2169 deletions

View File

@@ -24,11 +24,40 @@
</div>
<template v-for="(info, key) in userInfo" :key="key">
<div v-if="info.condition">
<label class="text-secondaryDark" :for="key">{{ info.label }}</label>
<div v-if="key === 'displayName'" class="flex flex-col space-y-3">
<label class="text-accentContrast" for="teamname"
>{{ t('users.name') }}
</label>
<div
class="w-full p-3 mt-2 bg-divider border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
class="flex bg-divider rounded-md items-stretch flex-1 border border-divider"
:class="{
'!border-accent': isNameBeingEdited,
}"
>
<HoppSmartInput
v-model="updatedUserName"
styles="bg-transparent flex-1 rounded-md !rounded-r-none disabled:select-none border-r-0 disabled:cursor-default disabled:opacity-50"
:placeholder="t('users.name')"
:disabled="!isNameBeingEdited"
>
<template #button>
<HoppButtonPrimary
class="!rounded-l-none"
filled
:icon="isNameBeingEdited ? IconSave : IconEdit"
:label="
isNameBeingEdited ? t('users.rename') : t('users.edit')
"
@click="handleNameEdit"
/>
</template>
</HoppSmartInput>
</div>
</div>
<div v-else-if="info.condition">
<label class="text-secondaryDark" :for="key">{{ info.label }}</label>
<div class="w-full p-3 mt-2 bg-divider border-gray-600 rounded-md">
<span>{{ info.value }}</span>
</div>
</div>
@@ -70,10 +99,17 @@
</template>
<script setup lang="ts">
import { useMutation } from '@urql/vue';
import { format } from 'date-fns';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { UserInfoQuery } from '~/helpers/backend/graphql';
import {
UpdateUserDisplayNameByAdminDocument,
UserInfoQuery,
} from '~/helpers/backend/graphql';
import IconEdit from '~icons/lucide/edit';
import IconSave from '~icons/lucide/save';
import IconTrash from '~icons/lucide/trash';
import IconUserCheck from '~icons/lucide/user-check';
import IconUserMinus from '~icons/lucide/user-minus';
@@ -89,6 +125,7 @@ const emit = defineEmits<{
(event: 'delete-user', userID: string): void;
(event: 'make-admin', userID: string): void;
(event: 'remove-admin', userID: string): void;
(event: 'update-user-name', newName: string): void;
}>();
// Get Proper Date Formats
@@ -120,4 +157,62 @@ const userInfo = {
value: getCreatedDateAndTime(createdOn),
},
};
// Contains the actual user name
const userName = computed({
get: () => props.user.displayName,
set: (value) => {
return value;
},
});
// Contains the stored user name from the actual name before being edited
const currentUserName = ref('');
// Set the current user name to the actual user name
onMounted(() => {
if (displayName) currentUserName.value = displayName;
});
// Contains the user name that is being edited
const updatedUserName = computed({
get: () => currentUserName.value,
set: (value) => {
currentUserName.value = value;
},
});
// Rename the user
const isNameBeingEdited = ref(false);
const userRename = useMutation(UpdateUserDisplayNameByAdminDocument);
const handleNameEdit = () => {
if (isNameBeingEdited.value) {
// If the name is not changed, then return control
if (userName.value !== updatedUserName.value) {
renameUserName();
} else isNameBeingEdited.value = false;
} else {
isNameBeingEdited.value = true;
}
};
const renameUserName = async () => {
if (updatedUserName.value?.trim() === '') {
toast.error(t('users.empty_name'));
return;
}
const variables = { userUID: uid, name: updatedUserName.value as string };
const result = await userRename.executeMutation(variables);
if (result.error) {
toast.error(t('state.rename_user_failure'));
} else {
isNameBeingEdited.value = false;
toast.success(t('state.rename_user_success'));
emit('update-user-name', updatedUserName.value as string);
userName.value = updatedUserName.value;
}
};
</script>

View File

@@ -10,6 +10,7 @@
v-model="email"
:label="t('users.email_address')"
input-styles="floating-input"
@submit="sendInvite"
/>
</template>
<template #footer>
@@ -18,7 +19,12 @@
:label="t('users.send_invite')"
@click="sendInvite"
/>
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
<HoppButtonSecondary
:label="t('users.cancel')"
outline
filled
@click="hideModal"
/>
</span>
</template>
</HoppSmartModal>

View File

@@ -1,78 +1,68 @@
<template>
<div class="px-4">
<div class="px-4 mt-7">
<div v-if="fetching" class="flex justify-center">
<HoppSmartSpinner />
</div>
<div v-else-if="error">{{ t('shared_requests.load_list_error') }}</div>
<div v-else-if="sharedRequests.length === 0" class="mt-5">
<div v-else-if="sharedRequests.length === 0">
{{ t('users.no_shared_requests') }}
</div>
<HoppSmartTable v-else class="mt-8" :list="sharedRequests">
<HoppSmartTable v-else :headings="headings" :list="sharedRequests">
<template #head>
<tr
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
>
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
<!-- Empty Heading for the Action Button -->
<th class="px-6 py-2 text-center">
{{ t('shared_requests.action') }}
</th>
</tr>
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
<!-- Empty Heading for the Action Button -->
<th class="px-6 py-2 text-center">
{{ t('shared_requests.action') }}
</th>
</template>
<template #body="{ list: sharedRequests }">
<tr
v-for="request in sharedRequests"
:key="request.id"
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
>
<td class="flex py-4 px-7 max-w-50">
<span class="truncate">
{{ request.id }}
</span>
</td>
<template #body="{ row: sharedRequest }">
<td class="flex py-4 px-7 max-w-50">
<span class="truncate">
{{ sharedRequest.id }}
</span>
</td>
<td class="py-4 px-7 w-96">
{{ sharedRequestURL(request.request) }}
</td>
<td class="py-4 px-7 w-96">
{{ sharedRequestURL(sharedRequest.request) }}
</td>
<td class="py-2 px-7">
{{ getCreatedDate(request.createdOn) }}
<div class="text-gray-400 text-tiny">
{{ getCreatedTime(request.createdOn) }}
</div>
</td>
<td class="py-2 px-7">
{{ getCreatedDate(sharedRequest.createdOn) }}
<div class="text-gray-400 text-tiny">
{{ getCreatedTime(sharedRequest.createdOn) }}
</div>
</td>
<td class="flex justify-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.open_request')"
:to="`${shortcodeBaseURL}/r/${request.id}`"
:blank="true"
:icon="IconExternalLink"
class="px-3 text-emerald-500 hover:text-accent"
/>
<td class="flex justify-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.open_request')"
:to="`${shortcodeBaseURL}/r/${sharedRequest.id}`"
:blank="true"
:icon="IconExternalLink"
class="px-3 text-emerald-500 hover:text-accent"
/>
<UiAutoResetIcon
:title="t('shared_requests.copy')"
:icon="{ default: IconCopy, temporary: IconCheck }"
@click="copySharedRequest(request.id)"
/>
<UiAutoResetIcon
:title="t('shared_requests.copy')"
:icon="{ default: IconCopy, temporary: IconCheck }"
@click="copySharedRequest(sharedRequest.id)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.delete')"
:icon="IconTrash"
color="red"
class="px-3"
@click="deleteSharedRequest(request.id)"
/>
</td>
</tr>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.delete')"
:icon="IconTrash"
color="red"
class="px-3"
@click="deleteSharedRequest(sharedRequest.id)"
/>
</td>
</template>
</HoppSmartTable>
@@ -136,11 +126,18 @@ const {
} = usePagedQuery(
SharedRequestsDocument,
(x) => x.infra.allShortcodes,
(x) => x.id,
sharedRequestsPerPage,
{ cursor: undefined, take: sharedRequestsPerPage, email: props.email }
{ cursor: undefined, take: sharedRequestsPerPage, email: props.email },
(x) => x.id
);
const headings = [
{ key: 'id', label: t('shared_requests.id') },
{ key: 'request', label: t('shared_requests.url') },
{ key: 'createdOn', label: t('shared_requests.created_on') },
{ key: 'action', label: t('shared_requests.action') },
];
// Return request endpoint from the request object
const sharedRequestURL = (request: string) => {
const parsedRequest = JSON.parse(request);
@@ -174,17 +171,17 @@ const deleteSharedRequestMutation = async (id: string | null) => {
return;
}
const variables = { codeID: id };
await sharedRequestDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(t('state.delete_request_failure'));
} else {
sharedRequests.value = sharedRequests.value.filter(
(request) => request.id !== id
);
refetch();
toast.success(t('state.delete_request_success'));
}
});
const result = await sharedRequestDeletion.executeMutation(variables);
if (result.error) {
toast.error(t('state.delete_request_failure'));
} else {
sharedRequests.value = sharedRequests.value.filter(
(request) => request.id !== id
);
refetch();
toast.success(t('state.delete_request_success'));
}
confirmDeletion.value = false;
deleteSharedRequestID.value = null;
};