feat: introducing i18n support to admin dashboard (#3051)

This commit is contained in:
Joel Jacob Stephen
2023-06-16 07:17:00 +03:00
committed by GitHub
parent b07243f131
commit 331d482b22
58 changed files with 905 additions and 184 deletions

View File

@@ -5,14 +5,18 @@
<div class="flex items-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
title="Open navigation"
:title="t('app.open_navigation')"
:icon="IconMenu"
class="transform !md:hidden mr-2"
@click="isOpen = true"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="isExpanded ? 'Collapse sidebar' : 'Expand sidebar'"
:title="
isExpanded
? `${t('app.collapse_sidebar')}`
: `${t('app.expand_sidebar')}`
"
:icon="isExpanded ? IconSidebarClose : IconSidebarOpen"
class="transform"
@click="expandSidebar"
@@ -34,13 +38,21 @@
theme: 'tooltip',
}"
:url="currentUser.photoURL"
:alt="currentUser.displayName ?? 'No Name'"
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
:alt="currentUser.displayName ?? `${t('app.no_name')}`"
:title="
currentUser.displayName ??
currentUser.email ??
`${t('app.no_name')}`
"
/>
<HoppSmartPicture
v-else
v-tippy="{ theme: 'tooltip' }"
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
:title="
currentUser.displayName ??
currentUser.email ??
`${t('app.no_name')}`
"
:initial="currentUser.displayName ?? currentUser.email"
/>
<template #content="{ hide }">
@@ -70,6 +82,9 @@ import { auth } from '~/helpers/auth';
import IconMenu from '~icons/lucide/menu';
import IconSidebarOpen from '~icons/lucide/sidebar-open';
import IconSidebarClose from '~icons/lucide/sidebar-close';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const { isOpen, isExpanded } = useSidebar();

View File

@@ -131,6 +131,9 @@ import { setLocalConfig } from '~/helpers/localpersistence';
import { useStreamSubscriber } from '~/composables/stream';
import { useToast } from '~/composables/toast';
import { auth } from '~/helpers/auth';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const { subscribeToStream } = useStreamSubscriber();
@@ -158,7 +161,7 @@ onMounted(() => {
subscribeToStream(currentUser$, (user) => {
if (user && !user.isAdmin) {
nonAdminUser.value = true;
toast.error(`You are logged in. But you're not an admin`);
toast.error(`${t('state.non_admin_login')}`);
}
});
});
@@ -174,7 +177,7 @@ async function signInWithGoogle() {
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
*/
toast.error(`Failed to sign in with Google`);
toast.error(`${t('state.google_signin_failure')}`);
}
signingInWithGoogle.value = false;
@@ -190,7 +193,7 @@ async function signInWithGithub() {
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
*/
toast.error(`Failed to sign in with GitHub`);
toast.error(`${t('state.github_signin_failure')}`);
}
signingInWithGitHub.value = false;
@@ -211,7 +214,7 @@ async function signInWithMicrosoft() {
@firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set
They may be related to https://github.com/firebase/firebaseui-web/issues/947
*/
toast.error(`Something went wrong`);
toast.error(`${t('state.error')}`);
}
signingInWithMicrosoft.value = false;
@@ -239,10 +242,10 @@ const logout = async () => {
try {
await auth.signOutUser();
window.location.reload();
toast.success(`Logged out`);
toast.success(`${t('state.logged_out')}`);
} catch (e) {
console.error(e);
toast.error(`Something went wrong`);
toast.error(`${t('state.error')}`);
}
};
</script>

View File

@@ -2,14 +2,14 @@
<div class="flex" @click="openLogoutModal()">
<HoppSmartItem
:icon="IconLogOut"
:label="'Logout'"
:label="t('state.logout')"
:outline="outline"
:shortcut="shortcut"
@click="openLogoutModal()"
/>
<HoppSmartConfirmModal
:show="confirmLogout"
:title="`Confirm Logout`"
:title="t('state.confirm_logout')"
@hide-modal="confirmLogout = false"
@resolve="logout"
/>
@@ -22,6 +22,9 @@ import IconLogOut from '~icons/lucide/log-out';
import { useToast } from '~/composables/toast';
import { useRouter } from 'vue-router';
import { auth } from '~/helpers/auth';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const router = useRouter();
@@ -48,10 +51,10 @@ const logout = async () => {
try {
await auth.signOutUser();
router.push(`/`);
toast.success(`Logged out`);
toast.success(`${t('state.logged_out')}`);
} catch (e) {
console.error(e);
toast.error(`Something went wrong`);
toast.error(`${t('state.error')}`);
}
};

View File

@@ -18,8 +18,10 @@
<HoppSmartLink class="flex items-center space-x-4" to="/dashboard">
<img src="/cover.jpg" alt="hoppscotch-logo" class="h-7" />
<span v-if="isExpanded" class="font-semibold text-accentContrast"
>HOPPSCOTCH</span
<span
v-if="isExpanded"
class="font-semibold text-accentContrast"
>{{ t('app.name') }}</span
>
</HoppSmartLink>
</div>
@@ -59,28 +61,31 @@
<script setup lang="ts">
import { HoppSmartLink } from '@hoppscotch/ui';
import { useSidebar } from '../../composables/useSidebar';
import { useSidebar } from '~/composables/useSidebar';
import IconDashboard from '~icons/lucide/layout-dashboard';
import IconUser from '~icons/lucide/user';
import IconUsers from '~icons/lucide/users';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const { isOpen, isExpanded } = useSidebar();
const primaryNavigations = [
{
label: 'Dashboard',
label: t('metrics.dashboard'),
icon: IconDashboard,
to: '/dashboard',
exact: true,
},
{
label: 'Users',
label: t('users.users'),
icon: IconUser,
to: '/users',
exact: false,
},
{
label: 'Teams',
label: t('teams.teams'),
icon: IconUsers,
to: '/teams',
exact: false,

View File

@@ -2,13 +2,13 @@
<HoppSmartModal
v-if="show"
dialog
title="Create team"
:title="t('teams.create_team')"
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col space-y-4 relative">
<div class="flex flex-col relaive">
<label for="teamName" class="py-2"> Team owner email </label>
<label for="teamName" class="py-2"> {{ t('teams.email') }} </label>
<HoppSmartAutoComplete
styles="w-full p-2 bg-transparent border border-divider rounded-md "
class="flex-1 !flex"
@@ -19,7 +19,7 @@
/>
</div>
<div class="flex flex-col">
<label for="teamName" class="py-2">Team name</label>
<label for="teamName" class="py-2">{{ t('teams.name') }} </label>
<input
id="teamName"
v-model="teamName"
@@ -35,11 +35,16 @@
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
label="Create team"
:label="t('teams.create_team')"
:loading="loadingState"
@click="createTeam"
/>
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
<HoppButtonSecondary
:label="t('teams.cancel')"
outline
filled
@click="hideModal"
/>
</span>
</template>
</HoppSmartModal>
@@ -48,6 +53,9 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useToast } from '~/composables/toast';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
@@ -75,11 +83,11 @@ const getOwnerEmail = (email: string) => (ownerEmail.value = email);
const createTeam = () => {
if (teamName.value.trim() === '') {
toast.error('Please enter a valid team name');
toast.error(`${t('teams.valid_name')}`);
return;
}
if (ownerEmail.value.trim() === '') {
toast.error('Please enter a valid owner email');
toast.error(`${t('teams.valid_owner_email')}`);
return;
}
emit('create-team', teamName.value, ownerEmail.value);

View File

@@ -2,14 +2,18 @@
<div class="flex flex-col">
<div class="flex flex-col space-y-8">
<div v-if="team.id" class="flex flex-col space-y-3">
<label class="text-accentContrast" for="username">Team ID</label>
<label class="text-accentContrast" for="username"
>{{ t('teams.id') }}
</label>
<div class="w-full p-3 bg-divider rounded-md">
{{ team.id }}
</div>
</div>
<div v-if="teamName" class="flex flex-col space-y-3">
<label class="text-accentContrast" for="teamname">Team Name </label>
<label class="text-accentContrast" for="teamname"
>{{ t('teams.name') }}
</label>
<div
class="flex bg-divider rounded-md items-stretch flex-1 border border-divider"
:class="{
@@ -29,7 +33,9 @@
class="!rounded-l-none"
filled
:icon="showRenameInput ? IconSave : IconEdit"
:label="showRenameInput ? 'Rename' : 'Edit'"
:label="
showRenameInput ? `${t('teams.rename')}` : `${t('teams.edit')}`
"
@click="handleNameEdit()"
/>
</div>
@@ -37,8 +43,8 @@
<div v-if="team.teamMembers.length" class="flex flex-col space-y-3">
<label class="text-accentContrast" for="username"
>Number of Members</label
>
>{{ t('teams.members') }}
</label>
<div class="w-full p-3 bg-divider rounded-md">
{{ team.teamMembers.length }}
</div>
@@ -49,7 +55,7 @@
<HoppButtonPrimary
class="!bg-red-600 !hover:opacity-80"
filled
label="Delete Team"
:label="t('teams.delete_team')"
@click="team && $emit('delete-team', team.id)"
:icon="IconTrash"
/>
@@ -58,12 +64,15 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ref } from 'vue';
import { useToast } from '~/composables/toast';
import { TeamInfoQuery } from '~/helpers/backend/graphql';
import IconEdit from '~icons/lucide/edit';
import IconSave from '~icons/lucide/save';
import IconTrash from '~icons/lucide/trash-2';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
@@ -91,7 +100,7 @@ const handleNameEdit = () => {
const renameTeam = () => {
if (newTeamName.value.trim() === '') {
toast.error('Team name cannot be empty');
toast.error(`${t('teams.empty_name')}`);
return;
}
emit('rename-team', newTeamName.value);

View File

@@ -115,11 +115,6 @@
v-if="newMembersList.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/dark/add_group.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
/>
<span class="pb-4 text-center"> No invites </span>
<HoppButtonSecondary label="Add new" filled @click="addNewMember" />
</div>
@@ -131,7 +126,9 @@
<span
class="flex items-center justify-center px-2 py-1 mb-4 font-semibold border rounded-full bg-primaryDark border-divider"
>
<icon-lucide-help-circle class="mr-2 text-secondaryLight svg-icons" />
<icon-lucide-help-circle
class="mr-2 text-secondaryLight svg-icons"
/>
Roles
</span>
<p>
@@ -209,6 +206,9 @@ import IconCircleDot from '~icons/lucide/circle-dot';
import IconCircle from '~icons/lucide/circle';
import { computed } from 'vue';
import { usePagedQuery } from '~/composables/usePagedQuery';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
// Get Users List
const { data } = useQuery({ query: MetricsDocument });
@@ -286,7 +286,7 @@ const addUserasTeamMember = async () => {
if (O.isNone(validationResult)) {
// Error handling for no validation
toast.error('Invalid User!!');
toast.error(`${t('users.invalid_user')}`);
addingUserToTeam.value = false;
return;
}
@@ -318,12 +318,12 @@ const addUserToTeam = async (
.then((result) => {
if (result.error) {
if (result.error.toString() == '[GraphQL] user/not_found') {
toast.error('User not found in the infra!!');
toast.error(`${t('state.user_not_found')}`);
} else {
toast.error('Failed to add user to the team!!');
toast.error(`${t('state.add_user_failure')}`);
}
} else {
toast.success('User is now a member of the team!!');
toast.success(`${t('state.add_user_success')}`);
emit('member');
}
});

View File

@@ -4,7 +4,7 @@
<div class="flex">
<HoppButtonPrimary
:icon="IconUserPlus"
label="Add Members"
:label="t('teams.add_members')"
filled
@click="showInvite = !showInvite"
/>
@@ -16,11 +16,11 @@
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<span class="pb-4 text-center">
No members in this team. Add members to this team to collaborate
{{ t('teams.no_members') }}
</span>
<HoppButtonSecondary
:icon="IconUserPlus"
label="Add Members"
:label="t('teams.add_members')"
@click="
() => {
showInvite = !showInvite;
@@ -122,7 +122,7 @@
<HoppButtonSecondary
id="member"
v-tippy="{ theme: 'tooltip' }"
title="Remove"
:title="t('teams.remove')"
:icon="IconUserMinus"
color="red"
:loading="isLoadingIndex === index"
@@ -134,12 +134,16 @@
</div>
<div v-if="!fetching && !team" class="flex flex-col items-center">
<icon-lucide-help-circle class="mb-4 svg-icons" />
Something went wrong. Please try again later.
{{ t('teams.error') }}
</div>
</div>
<div class="flex">
<HoppButtonPrimary label="Save" outline @click="saveUpdatedTeam" />
<HoppButtonPrimary
:label="t('teams.save')"
outline
@click="saveUpdatedTeam"
/>
</div>
<TeamsInvite
:show="showInvite"
@@ -163,7 +167,7 @@ import IconChevronDown from '~icons/lucide/chevron-down';
import { useClientHandle, useMutation } from '@urql/vue';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useToast } from '../../composables/toast';
import { useToast } from '~/composables/toast';
import {
ChangeUserRoleInTeamByAdminDocument,
TeamInfoDocument,
@@ -172,6 +176,9 @@ import {
TeamInfoQuery,
} from '../../helpers/backend/graphql';
import { HoppButtonPrimary, HoppButtonSecondary } from '@hoppscotch/ui';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
@@ -195,7 +202,7 @@ const getTeamInfo = async () => {
.toPromise();
if (result.error) {
return toast.error('Unable to Load Team Info..');
return toast.error(`${t('teams.load_info_error')}`);
}
if (result.data?.admin.teamInfo) {
team.value = result.data.admin.teamInfo;
@@ -301,10 +308,10 @@ const saveUpdatedTeam = async () => {
update.role
);
if (updateMemberRoleResult.error) {
toast.error('Role updation has failed!!');
toast.error(`${t('state.role_update_failed')}`);
roleUpdates.value = [];
} else {
toast.success('Roles updated successfully!!');
toast.success(`${t('state.role_update_success')}`);
roleUpdates.value = [];
}
isLoading.value = false;
@@ -334,12 +341,12 @@ const removeExistingTeamMember = async (userID: string, index: number) => {
team.value.id
)();
if (removeTeamMemberResult.error) {
toast.error(`Member couldn't be removed!!`);
toast.error(`${t('state.remove_member_failure')}`);
} else {
team.value.teamMembers = team.value.teamMembers?.filter(
(member: any) => member.user.uid !== userID
);
toast.success('Member removed successfully!!');
toast.success(`${t('state.remove_member_success')}`);
}
isLoadingIndex.value = null;
emit('update-team');

View File

@@ -28,7 +28,7 @@
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
title="Remove"
:title="t('teams.remove')"
:icon="IconTrash"
color="red"
:loading="isLoadingIndex === index"
@@ -41,11 +41,11 @@
v-if="team && pendingInvites?.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<span class="text-center"> No pending invites </span>
<span class="text-center">{{ t('teams.no_pending_invites') }} </span>
</div>
<div v-if="!fetching && error" class="flex flex-col items-center p-4">
<icon-lucide-help-circle class="mb-4 svg-icons" />
Something went wrong. Please try again later.
{{ t('teams.error') }}
</div>
</div>
</div>
@@ -62,6 +62,9 @@ import {
} from '~/helpers/backend/graphql';
import { useToast } from '~/composables/toast';
import { useRoute } from 'vue-router';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
@@ -83,7 +86,7 @@ const getTeamInfo = async () => {
if (result.error) {
error.value = true;
return toast.error('Unable to load team details..');
return toast.error(`${t('teams.load_info_error')}`);
}
if (result.data?.admin.teamInfo) {
@@ -106,7 +109,7 @@ const removeInvitee = async (id: string, index: number) => {
isLoadingIndex.value = index;
const result = await revokeTeamInvitation(id);
if (result.error) {
toast.error('Removal of invitee failed!!');
toast.error(`${t('state.remove_invitee_failure')}`);
} else {
if (pendingInvites.value) {
pendingInvites.value = pendingInvites.value.filter(
@@ -114,7 +117,7 @@ const removeInvitee = async (id: string, index: number) => {
return invite.id !== id;
}
);
toast.success('Removal of invitee is successfull!!');
toast.success(`${t('state.remove_invitee_success')}`);
}
}
isLoadingIndex.value = null;

View File

@@ -2,7 +2,7 @@
<HoppSmartModal
v-if="show"
dialog
title="Invite User"
:title="t('users.invite_user')"
@close="$emit('hide-modal')"
>
<template #body>
@@ -17,13 +17,13 @@
autocomplete="off"
@keyup.enter="sendInvite"
/>
<label for="inviteUserEmail">Email Address</label>
<label for="inviteUserEmail">{{ t('users.email_address') }}</label>
</div>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
label="Send Invite"
:label="t('users.send_invite')"
:loading="loadingState"
@click="sendInvite"
/>
@@ -36,6 +36,9 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useToast } from '~/composables/toast';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
@@ -59,7 +62,7 @@ const email = ref('');
const sendInvite = () => {
if (email.value.trim() === '') {
toast.error('Please enter a valid email address');
toast.error(`${t('users.valid_email')}`);
return;
}
emit('send-invite', email.value);

View File

@@ -2,10 +2,10 @@
<table class="w-full">
<thead>
<tr class="text-secondary border-b border-dividerDark text-sm text-left">
<th class="px-3 pb-3">User ID</th>
<th class="px-3 pb-3">Name</th>
<th class="px-3 pb-3">Email</th>
<th class="px-3 pb-3">Date</th>
<th class="px-3 pb-3">{{ t('users.id') }}</th>
<th class="px-3 pb-3">{{ t('users.name') }}</th>
<th class="px-3 pb-3">{{ t('users.email') }}</th>
<th class="px-3 pb-3">{{ t('users.date') }}</th>
<th class="px-3 pb-3"></th>
</tr>
</thead>
@@ -36,16 +36,16 @@
v-if="user.isAdmin"
class="text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300"
>
Admin
{{ t('users.admin') }}
</span>
</div>
<div v-else class="flex items-center space-x-3">
<span> (Unnamed user) </span>
<span> {{ t('users.unnamed') }} </span>
<span
v-if="user.isAdmin"
class="text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300"
>
Admin
{{ t('users.admin') }}
</span>
</div>
</td>
@@ -138,6 +138,9 @@ import IconUserCheck from '~icons/lucide/user-check';
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
import { UsersListQuery } from '~/helpers/backend/graphql';
import { TippyComponent } from 'vue-tippy';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
defineProps<{
usersList: UsersListQuery['admin']['allUsers'];