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

@@ -0,0 +1 @@
./node_modules

View File

@@ -0,0 +1,195 @@
[
{
"code": "af",
"file": "af.json",
"iso": "af-AF",
"name": "Afrikaans"
},
{
"code": "ar",
"dir": "rtl",
"file": "ar.json",
"iso": "ar-AR",
"name": "عربى"
},
{
"code": "ca",
"file": "ca.json",
"iso": "ca-CA",
"name": "Català"
},
{
"code": "cn",
"file": "cn.json",
"iso": "zh-CN",
"name": "简体中文"
},
{
"code": "cs",
"file": "cs.json",
"iso": "cs-CS",
"name": "Čeština"
},
{
"code": "da",
"file": "da.json",
"iso": "da-DA",
"name": "Dansk"
},
{
"code": "de",
"file": "de.json",
"iso": "de-DE",
"name": "Deutsch"
},
{
"code": "el",
"file": "el.json",
"iso": "el-EL",
"name": "Ελληνικά"
},
{
"code": "en",
"file": "en.json",
"iso": "en-US",
"name": "English"
},
{
"code": "es",
"file": "es.json",
"iso": "es-ES",
"name": "Español"
},
{
"code": "fi",
"file": "fi.json",
"iso": "fi-FI",
"name": "Suomalainen"
},
{
"code": "fr",
"file": "fr.json",
"iso": "fr-FR",
"name": "Français"
},
{
"code": "he",
"file": "he.json",
"iso": "he-HE",
"name": "עִברִית"
},
{
"code": "hi",
"file": "hi.json",
"iso": "hi-HI",
"name": "हिन्दी"
},
{
"code": "hu",
"file": "hu.json",
"iso": "hu-HU",
"name": "Magyar"
},
{
"code": "id",
"file": "id.json",
"iso": "id",
"name": "Indonesian"
},
{
"code": "it",
"file": "it.json",
"iso": "it",
"name": "Italiano"
},
{
"code": "ja",
"file": "ja.json",
"iso": "ja-JA",
"name": "日本語"
},
{
"code": "ko",
"file": "ko.json",
"iso": "ko-KO",
"name": "한국어"
},
{
"code": "nl",
"file": "nl.json",
"iso": "nl-NL",
"name": "Nederlands"
},
{
"code": "no",
"file": "no.json",
"iso": "no-NO",
"name": "Norsk"
},
{
"code": "pl",
"file": "pl.json",
"iso": "pl-PL",
"name": "Polskie"
},
{
"code": "pt-br",
"file": "pt-br.json",
"iso": "pt-BR",
"name": "Português Brasileiro"
},
{
"code": "pt",
"file": "pt.json",
"iso": "pt-PT",
"name": "Português"
},
{
"code": "ro",
"file": "ro.json",
"iso": "ro-RO",
"name": "Română"
},
{
"code": "ru",
"file": "ru.json",
"iso": "ru-RU",
"name": "Pусский"
},
{
"code": "sr",
"file": "sr.json",
"iso": "sr-SR",
"name": "Српски"
},
{
"code": "sv",
"file": "sv.json",
"iso": "sv-SV",
"name": "Svenska"
},
{
"code": "tr",
"file": "tr.json",
"iso": "tr-TR",
"name": "Türkçe"
},
{
"code": "tw",
"file": "tw.json",
"iso": "zh-TW",
"name": "繁體中文"
},
{
"code": "uk",
"file": "uk.json",
"iso": "uk-UK",
"name": "Українська"
},
{
"code": "vi",
"file": "vi.json",
"iso": "vi-VI",
"name": "Tiếng Việt"
}
]

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,132 @@
{
"app": {
"collapse_sidebar": "Collapse Sidebar",
"expand_sidebar": "Expand Sidebar",
"name": "HOPPSCOTCH",
"no_name": "No name",
"open_navigation": "Open Navigation"
},
"metrics": {
"dashboard": "Dashboard",
"no_metrics": "No metrics found",
"total_collections": "Total Collections",
"total_requests": "Total Requests",
"total_teams": "Total Teams",
"total_users": "Total Users"
},
"role": {
"editor": "EDITOR",
"owner": "OWNER",
"viewer": "VIEWER"
},
"state": {
"add_user_failure": "Failed to add user to the team!!",
"add_user_success": "User is now a member of the team!!",
"admin_failure": "Failed to make user an admin!!",
"admin_success": "User is now an admin!!",
"confirm_logout": "Confirm Logout",
"create_team_failure": "Failed to create team!!",
"create_team_success": "Team created successfully!!",
"delete_team_failure": "Team deletion failed!!",
"delete_team_success": "Team deleted successfully!!",
"delete_user_failure": "User deletion failed!!",
"delete_user_success": "User deleted successfully!!",
"email_failure": "Failed to send invitation",
"email_success": "Email invitation sent successfully",
"enter_team_email": "Please enter email of team owner!!",
"error": "Something went wrong",
"github_signin_failure": "Failed to login with Github",
"google_signin_failure": "Failed to login with Google",
"invalid_email": "Please enter a valid email address",
"logged_out": "Logged out",
"logout": "Logout",
"non_admin_login": "You are logged in. But you're not an admin",
"remove_admin_failure": "Failed to remove admin status!!",
"remove_admin_success": "Admin status removed!!",
"remove_admin_to_delete_user": "Remove admin privilege to delete the user!!",
"remove_invitee_failure": "Removal of invitee failed!!",
"remove_invitee_success": "Removal of invitee is successfull!!",
"remove_member_failure": "Member couldn't be removed!!",
"remove_member_success": "Member removed successfully!!",
"rename_team_failure": "Failed to rename team!!",
"rename_team_success": "Team renamed successfully!",
"role_update_failed": "Roles updation has failed!!",
"role_update_success": "Roles updated successfully!!",
"team_name_long": "Team name should be atleast 6 characters long!!",
"user_not_found": "User not found in the infra!!"
},
"teams": {
"add_members": "Add Members",
"admin": "Admin",
"admin_Email": "Admin Email",
"admin_id": "Admin ID",
"cancel": "Cancel",
"confirm_team_deletion": "Confirm Deletion of the team?",
"create_team": "Create team",
"date": "Date",
"delete_team": "Delete Team",
"details": "Details",
"edit": "Edit",
"email": "Team owner email",
"email_address": "Email Address",
"error": "Something went wrong. Please try again later.",
"id": "Team ID",
"invited_email": "Invitee Email",
"invited_on": "Invited On",
"invites": "Invites",
"load_info_error": "Unable to load team info",
"load_list_error": "Unable to Load Teams List",
"members": "Number of members",
"name": "Team name",
"no_members": "No members in this team. Add members to this team to collaborate",
"no_pending_invites": "No pending invites",
"pending_invites": "Pending invites",
"remove": "Remove",
"rename": "Rename",
"save": "Save",
"send_invite": "Send Invite",
"show_more": "Show more",
"team_details": "Team details",
"team_members": "Members",
"team_members_tab": "Team members",
"teams": "Teams",
"uid": "UID",
"valid_name": "Please enter a valid team name",
"valid_owner_email": "Please enter a valid owner email"
},
"users": {
"admin": "Admin",
"admin_email": "Admin Email",
"admin_id": "Admin ID",
"confirm_admin_to_user": "Do you want to remove admin status from this user?",
"confirm_user_deletion": "Confirm user deletion?",
"confirm_user_to_admin": "Do you want to make this user into an admin?",
"created_on": "Created On",
"date": "Date",
"delete": "Delete",
"email": "Email",
"email_address": "Email Address",
"id": "User ID",
"invite_user": "Invite User",
"invited_on": "Invited On",
"invitee_email": "Invitee Email",
"invited_users": "Invited Users",
"invalid_user": "Invalid User",
"load_info_error": "Unable to load user info",
"load_list_error": "Unable to Load Users List",
"make_admin": "Make admin",
"name": "Name",
"no_invite": "No invited users found",
"no_users": "No users found",
"not_found": "User not found",
"remove_admin_privilege": "Remove Admin Privilege",
"remove_admin_status": "Remove Admin Status",
"send_invite": "Send Invite",
"show_more": "Show more",
"uid": "UID",
"unnamed": "(Unnamed User)",
"user_not_found": "User not found in the infra!!",
"users": "Users",
"valid_email": "Please enter a valid email address"
}
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -30,12 +30,13 @@
"io-ts": "^2.2.16", "io-ts": "^2.2.16",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"rxjs": "^7.8.0", "rxjs": "^7.8.0",
"tippy.js": "^6.3.7",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"unplugin-icons": "^0.14.9", "unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.21.0", "unplugin-vue-components": "^0.21.0",
"vue": "^3.2.6", "vue": "^3.2.6",
"vue-i18n": "^9.2.2",
"vue-router": "4", "vue-router": "4",
"tippy.js": "^6.3.7",
"vue-tippy": "6.0.0-alpha.58" "vue-tippy": "6.0.0-alpha.58"
}, },
"devDependencies": { "devDependencies": {
@@ -47,6 +48,7 @@
"@graphql-codegen/typescript-document-nodes": "3.0.0", "@graphql-codegen/typescript-document-nodes": "3.0.0",
"@graphql-codegen/typescript-operations": "3.0.0", "@graphql-codegen/typescript-operations": "3.0.0",
"@graphql-codegen/urql-introspection": "2.2.1", "@graphql-codegen/urql-introspection": "2.2.1",
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@vitejs/plugin-vue": "^3.1.0", "@vitejs/plugin-vue": "^3.1.0",
"@vue/compiler-sfc": "^3.2.6", "@vue/compiler-sfc": "^3.2.6",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",

View File

@@ -14,14 +14,6 @@ declare module '@vue/runtime-core' {
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'] AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
AppToast: typeof import('./components/app/Toast.vue')['default'] AppToast: typeof import('./components/app/Toast.vue')['default']
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'] DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideUser: typeof import('~icons/lucide/user')['default']
ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
TeamsAdd: typeof import('./components/teams/Add.vue')['default'] TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsDetails: typeof import('./components/teams/Details.vue')['default'] TeamsDetails: typeof import('./components/teams/Details.vue')['default']
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'] TeamsInvite: typeof import('./components/teams/Invite.vue')['default']

View File

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

View File

@@ -131,6 +131,9 @@ import { setLocalConfig } from '~/helpers/localpersistence';
import { useStreamSubscriber } from '~/composables/stream'; import { useStreamSubscriber } from '~/composables/stream';
import { useToast } from '~/composables/toast'; import { useToast } from '~/composables/toast';
import { auth } from '~/helpers/auth'; import { auth } from '~/helpers/auth';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const { subscribeToStream } = useStreamSubscriber(); const { subscribeToStream } = useStreamSubscriber();
@@ -158,7 +161,7 @@ onMounted(() => {
subscribeToStream(currentUser$, (user) => { subscribeToStream(currentUser$, (user) => {
if (user && !user.isAdmin) { if (user && !user.isAdmin) {
nonAdminUser.value = true; 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 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 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; 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 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 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; signingInWithGitHub.value = false;
@@ -211,7 +214,7 @@ async function signInWithMicrosoft() {
@firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set @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 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; signingInWithMicrosoft.value = false;
@@ -239,10 +242,10 @@ const logout = async () => {
try { try {
await auth.signOutUser(); await auth.signOutUser();
window.location.reload(); window.location.reload();
toast.success(`Logged out`); toast.success(`${t('state.logged_out')}`);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
toast.error(`Something went wrong`); toast.error(`${t('state.error')}`);
} }
}; };
</script> </script>

View File

@@ -2,14 +2,14 @@
<div class="flex" @click="openLogoutModal()"> <div class="flex" @click="openLogoutModal()">
<HoppSmartItem <HoppSmartItem
:icon="IconLogOut" :icon="IconLogOut"
:label="'Logout'" :label="t('state.logout')"
:outline="outline" :outline="outline"
:shortcut="shortcut" :shortcut="shortcut"
@click="openLogoutModal()" @click="openLogoutModal()"
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmLogout" :show="confirmLogout"
:title="`Confirm Logout`" :title="t('state.confirm_logout')"
@hide-modal="confirmLogout = false" @hide-modal="confirmLogout = false"
@resolve="logout" @resolve="logout"
/> />
@@ -22,6 +22,9 @@ import IconLogOut from '~icons/lucide/log-out';
import { useToast } from '~/composables/toast'; import { useToast } from '~/composables/toast';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { auth } from '~/helpers/auth'; import { auth } from '~/helpers/auth';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const router = useRouter(); const router = useRouter();
@@ -48,10 +51,10 @@ const logout = async () => {
try { try {
await auth.signOutUser(); await auth.signOutUser();
router.push(`/`); router.push(`/`);
toast.success(`Logged out`); toast.success(`${t('state.logged_out')}`);
} catch (e) { } catch (e) {
console.error(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"> <HoppSmartLink class="flex items-center space-x-4" to="/dashboard">
<img src="/cover.jpg" alt="hoppscotch-logo" class="h-7" /> <img src="/cover.jpg" alt="hoppscotch-logo" class="h-7" />
<span v-if="isExpanded" class="font-semibold text-accentContrast" <span
>HOPPSCOTCH</span v-if="isExpanded"
class="font-semibold text-accentContrast"
>{{ t('app.name') }}</span
> >
</HoppSmartLink> </HoppSmartLink>
</div> </div>
@@ -59,28 +61,31 @@
<script setup lang="ts"> <script setup lang="ts">
import { HoppSmartLink } from '@hoppscotch/ui'; import { HoppSmartLink } from '@hoppscotch/ui';
import { useSidebar } from '../../composables/useSidebar'; import { useSidebar } from '~/composables/useSidebar';
import IconDashboard from '~icons/lucide/layout-dashboard'; import IconDashboard from '~icons/lucide/layout-dashboard';
import IconUser from '~icons/lucide/user'; import IconUser from '~icons/lucide/user';
import IconUsers from '~icons/lucide/users'; import IconUsers from '~icons/lucide/users';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const { isOpen, isExpanded } = useSidebar(); const { isOpen, isExpanded } = useSidebar();
const primaryNavigations = [ const primaryNavigations = [
{ {
label: 'Dashboard', label: t('metrics.dashboard'),
icon: IconDashboard, icon: IconDashboard,
to: '/dashboard', to: '/dashboard',
exact: true, exact: true,
}, },
{ {
label: 'Users', label: t('users.users'),
icon: IconUser, icon: IconUser,
to: '/users', to: '/users',
exact: false, exact: false,
}, },
{ {
label: 'Teams', label: t('teams.teams'),
icon: IconUsers, icon: IconUsers,
to: '/teams', to: '/teams',
exact: false, exact: false,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
import { flow } from "fp-ts/function"
import { useI18n as _useI18n } from "vue-i18n"
export const useI18n = flow(_useI18n, (x) => x.t)
export const useFullI18n = _useI18n

View File

@@ -0,0 +1,3 @@
export const throwError = (message: string): never => {
throw new Error(message)
}

View File

@@ -0,0 +1,154 @@
import * as R from 'fp-ts/Record';
import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
import { createI18n, I18n, I18nOptions } from 'vue-i18n';
import { HoppModule } from '.';
import languages from '../../languages.json';
import { throwError } from '../helpers/error';
import { getLocalConfig, setLocalConfig } from '../helpers/localpersistence';
/*
In context of this file, we have 2 main kinds of things.
1. Locale -> A locale is termed as the i18n entries present in the /locales folder
2. Language -> A language is an entry in the /languages.json folder
Each language entry should correspond to a locale entry.
*/
/*
* As we migrate out of Nuxt I18n into our own system for i18n management,
* Some stuff has changed regarding how it works.
*
* The previous system works by using paths to represent locales to load.
* Basically, /es/realtime will load the /realtime page but with 'es' language
*
* In the new system instead of relying on the lang code, we store the language
* in the application local config store (localStorage). The URLs don't have
* a locale path effect
*/
// TODO: Syncing into settings ?
const LOCALES = import.meta.glob('../../locales/*.json');
setTimeout(() => {
console.log(LOCALES);
}, 1000);
type LanguagesDef = {
code: string;
file: string;
iso: string;
name: string;
dir?: 'ltr' | 'rtl'; // Text Orientation (defaults to 'ltr')
};
const FALLBACK_LANG_CODE = 'en';
// TypeScript cannot understand dir is restricted to "ltr" or "rtl" yet, hence assertion
export const APP_LANGUAGES: LanguagesDef[] = languages as LanguagesDef[];
export const APP_LANG_CODES = languages.map(({ code }) => code);
export const FALLBACK_LANG = pipe(
APP_LANGUAGES,
A.findFirst((x) => x.code === FALLBACK_LANG_CODE),
O.getOrElseW(() =>
throwError(`Could not find the fallback language '${FALLBACK_LANG_CODE}'`)
)
);
// A reference to the i18n instance
let i18nInstance: I18n<any, any, any> | null = null;
const resolveCurrentLocale = () =>
pipe(
// Resolve from locale and make sure it is in languages
getLocalConfig('locale'),
O.fromNullable,
O.filter((locale) =>
pipe(
APP_LANGUAGES,
A.some(({ code }) => code === locale)
)
),
// Else load from navigator.language
O.alt(() =>
pipe(
APP_LANGUAGES,
A.findFirst(({ code }) => navigator.language.startsWith(code)), // en-US should also match to en
O.map(({ code }) => code)
)
),
// Else load fallback
O.getOrElse(() => FALLBACK_LANG_CODE)
);
/**
* Changes the application language. This function returns a promise as
* the locale files are lazy loaded on demand
* @param locale The locale code of the language to load
*/
export const changeAppLanguage = async (locale: string) => {
const localeData = (
(await pipe(
LOCALES,
R.lookup(`../../locales/${locale}.json`),
O.getOrElseW(() =>
throwError(
`Tried to change app language to non-existent locale '${locale}'`
)
)
)()) as any
).default;
if (!i18nInstance) {
throw new Error('Tried to change language without active i18n instance');
}
i18nInstance.global.setLocaleMessage(locale, localeData);
// TODO: Look into the type issues here
i18nInstance.global.locale.value = locale;
setLocalConfig('locale', locale);
};
export default <HoppModule>{
onVueAppInit(app) {
const i18n = createI18n(<I18nOptions>{
locale: 'en', // TODO: i18n system!
fallbackLocale: 'en',
legacy: false,
allowComposition: true,
});
app.use(i18n);
i18nInstance = i18n;
// TODO: Global loading state to hide the resolved lang loading
const currentLocale = resolveCurrentLocale();
changeAppLanguage(currentLocale);
setLocalConfig('locale', currentLocale);
},
onBeforeRouteChange(to, _, router) {
// Convert old locale path format to new format
const oldLocalePathLangCode = APP_LANG_CODES.find((langCode) =>
to.path.startsWith(`/${langCode}/`)
);
// Change language to the correct lang code
if (oldLocalePathLangCode) {
changeAppLanguage(oldLocalePathLangCode);
router.replace(to.path.substring(`/${oldLocalePathLangCode}`.length));
}
},
};

View File

@@ -1,38 +1,40 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="text-lg font-bold text-secondaryDark">Dashboard</h1> <h1 class="text-lg font-bold text-secondaryDark">
{{ t('metrics.dashboard') }}
</h1>
<div v-if="fetching" class="flex justify-center py-6"> <div v-if="fetching" class="flex justify-center py-6">
<HoppSmartSpinner /> <HoppSmartSpinner />
</div> </div>
<div v-else-if="error || !metrics"> <div v-else-if="error || !metrics">
<p class="text-xl">No Metrics Found..</p> <p class="text-xl">{{ t('metrics.no_metrics') }}</p>
</div> </div>
<div v-else> <div v-else>
<div class="py-10 grid lg:grid-cols-2 gap-6"> <div class="py-10 grid lg:grid-cols-2 gap-6">
<DashboardMetricsCard <DashboardMetricsCard
:count="metrics.usersCount" :count="metrics.usersCount"
label="Total Users" :label="t('metrics.total_users')"
:icon="UserIcon" :icon="UserIcon"
color="text-green-400" color="text-green-400"
/> />
<DashboardMetricsCard <DashboardMetricsCard
:count="metrics.teamsCount" :count="metrics.teamsCount"
label="Total Teams" :label="t('metrics.total_teams')"
:icon="UsersIcon" :icon="UsersIcon"
color="text-pink-400" color="text-pink-400"
/> />
<DashboardMetricsCard <DashboardMetricsCard
:count="metrics.teamRequestsCount" :count="metrics.teamRequestsCount"
label="Total Requests" :label="t('metrics.total_requests')"
:icon="LineChartIcon" :icon="LineChartIcon"
color="text-cyan-400" color="text-cyan-400"
/> />
<DashboardMetricsCard <DashboardMetricsCard
:count="metrics.teamCollectionsCount" :count="metrics.teamCollectionsCount"
label="Total Collections" :label="t('metrics.total_collections')"
:icon="FolderTreeIcon" :icon="FolderTreeIcon"
color="text-orange-400" color="text-orange-400"
/> />
@@ -49,6 +51,9 @@ import UserIcon from '~icons/lucide/user';
import UsersIcon from '~icons/lucide/users'; import UsersIcon from '~icons/lucide/users';
import LineChartIcon from '~icons/lucide/line-chart'; import LineChartIcon from '~icons/lucide/line-chart';
import FolderTreeIcon from '~icons/lucide/folder-tree'; import FolderTreeIcon from '~icons/lucide/folder-tree';
import { useI18n } from '../composables/i18n';
const t = useI18n();
// Get Metrics Data // Get Metrics Data
const { fetching, error, data } = useQuery({ query: MetricsDocument }); const { fetching, error, data } = useQuery({ query: MetricsDocument });

View File

@@ -25,7 +25,7 @@
<div class="py-8"> <div class="py-8">
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs> <HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
<HoppSmartTab :id="'details'" label="Details"> <HoppSmartTab :id="'details'" :label="t('teams.details')">
<TeamsDetails <TeamsDetails
:team="team" :team="team"
:teamName="teamName" :teamName="teamName"
@@ -35,17 +35,17 @@
class="py-8 px-4" class="py-8 px-4"
/> />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'members'" label="Members"> <HoppSmartTab :id="'members'" :label="t('teams.team_members')">
<TeamsMembers @update-team="updateTeam()" class="py-8 px-4" /> <TeamsMembers @update-team="updateTeam()" class="py-8 px-4" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'invites'" label="Invites"> <HoppSmartTab :id="'invites'" :label="t('teams.invites')">
<TeamsPendingInvites :editingTeamID="team.id" class="py-8 px-4" /> <TeamsPendingInvites :editingTeamID="team.id" class="py-8 px-4" />
</HoppSmartTab> </HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmDeletion" :show="confirmDeletion"
:title="`Confirm Deletion of ${team.name} team?`" :title="t('teams.confirm_team_deletion')"
@hide-modal="confirmDeletion = false" @hide-modal="confirmDeletion = false"
@resolve="deleteTeamMutation(deleteTeamUID)" @resolve="deleteTeamMutation(deleteTeamUID)"
/> />
@@ -58,7 +58,7 @@
import { useClientHandle, useMutation } from '@urql/vue'; import { useClientHandle, useMutation } from '@urql/vue';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useToast } from '../../composables/toast'; import { useToast } from '~/composables/toast';
import { import {
RemoveTeamDocument, RemoveTeamDocument,
RenameTeamDocument, RenameTeamDocument,
@@ -67,6 +67,9 @@ import {
TeamInfoQuery, TeamInfoQuery,
} from '../../helpers/backend/graphql'; } from '../../helpers/backend/graphql';
import { HoppSmartTabs } from '@hoppscotch/ui'; import { HoppSmartTabs } from '@hoppscotch/ui';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast(); const toast = useToast();
@@ -77,11 +80,11 @@ const selectedOptionTab = ref<OptionTabs>('details');
const currentTabName = computed(() => { const currentTabName = computed(() => {
switch (selectedOptionTab.value) { switch (selectedOptionTab.value) {
case 'details': case 'details':
return 'Team details'; return t('teams.team_details');
case 'members': case 'members':
return 'Team members'; return t('teams.team_members_tab');
case 'invites': case 'invites':
return 'Pending invites'; return t('teams.pending_invites');
default: default:
return ''; return '';
} }
@@ -100,7 +103,7 @@ const getTeamInfo = async () => {
.query(TeamInfoDocument, { teamID: route.params.id.toString() }) .query(TeamInfoDocument, { teamID: route.params.id.toString() })
.toPromise(); .toPromise();
if (result.error) { if (result.error) {
return toast.error('Unable to load team info..'); return toast.error(`${t('team.load_info_error')}`);
} }
if (result.data?.admin.teamInfo) { if (result.data?.admin.teamInfo) {
team.value = result.data.admin.teamInfo; team.value = result.data.admin.teamInfo;
@@ -127,12 +130,12 @@ const renameTeamName = async (teamName: string) => {
const variables = { uid: team.value.id, name: teamName }; const variables = { uid: team.value.id, name: teamName };
await teamRename.executeMutation(variables).then((result) => { await teamRename.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('Failed to rename team!!'); toast.error(`${t('state.rename_team_failure')}`);
} else { } else {
showRenameInput.value = false; showRenameInput.value = false;
if (team.value) { if (team.value) {
team.value.name = teamName; team.value.name = teamName;
toast.success('Team renamed successfully!!'); toast.success(`${t('state.rename_team_success')}`);
} }
} }
}); });
@@ -152,15 +155,15 @@ const deleteTeam = (id: string) => {
const deleteTeamMutation = async (id: string | null) => { const deleteTeamMutation = async (id: string | null) => {
if (!id) { if (!id) {
confirmDeletion.value = false; confirmDeletion.value = false;
toast.error('Team deletion failed!!'); toast.error(`${t('state.delete_team_failure')}`);
return; return;
} }
const variables = { uid: id }; const variables = { uid: id };
await teamDeletion.executeMutation(variables).then((result) => { await teamDeletion.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('Team deletion failed!!'); toast.error(`${t('state.delete_team_failure')}`);
} else { } else {
toast.success('Team deleted successfully!!'); toast.success(`${t('state.delete_team_success')}`);
} }
}); });
confirmDeletion.value = false; confirmDeletion.value = false;

View File

@@ -1,12 +1,12 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="text-lg font-bold text-secondaryDark">Teams</h1> <h1 class="text-lg font-bold text-secondaryDark">{{ t('teams.teams') }}</h1>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex py-10"> <div class="flex py-10">
<HoppButtonPrimary <HoppButtonPrimary
:icon="IconAddUsers" :icon="IconAddUsers"
label="Create team" :label="t('teams.create_team')"
@click="showCreateTeamModal = true" @click="showCreateTeamModal = true"
/> />
</div> </div>
@@ -19,7 +19,7 @@
<HoppSmartSpinner /> <HoppSmartSpinner />
</div> </div>
<div v-else-if="error">Unable to Load Teams List..</div> <div v-else-if="error">{{ t('teams.load_list_error') }}</div>
<TeamsTable <TeamsTable
v-else v-else
@@ -34,7 +34,7 @@
class="flex justify-center my-5 px-3 py-2 cursor-pointer font-semibold rounded-3xl bg-dividerDark hover:bg-divider transition mx-auto w-38 text-secondaryDark" class="flex justify-center my-5 px-3 py-2 cursor-pointer font-semibold rounded-3xl bg-dividerDark hover:bg-divider transition mx-auto w-38 text-secondaryDark"
@click="fetchNextTeams" @click="fetchNextTeams"
> >
<span>Show more </span> <span>{{ t('teams.show_more') }}</span>
<icon-lucide-chevron-down class="ml-2 text-lg" /> <icon-lucide-chevron-down class="ml-2 text-lg" />
</div> </div>
</div> </div>
@@ -50,7 +50,7 @@
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmDeletion" :show="confirmDeletion"
:title="`Confirm Deletion of the team?`" :title="t('teams.confirm_team_deletion')"
@hide-modal="confirmDeletion = false" @hide-modal="confirmDeletion = false"
@resolve="deleteTeamMutation(deleteTeamID)" @resolve="deleteTeamMutation(deleteTeamID)"
/> />
@@ -65,11 +65,14 @@ import {
TeamListDocument, TeamListDocument,
UsersListDocument, UsersListDocument,
} from '../../helpers/backend/graphql'; } from '../../helpers/backend/graphql';
import { usePagedQuery } from '../../composables/usePagedQuery'; import { usePagedQuery } from '~/composables/usePagedQuery';
import { ref, watch, computed } from 'vue'; import { ref, watch, computed } from 'vue';
import { useMutation, useQuery } from '@urql/vue'; import { useMutation, useQuery } from '@urql/vue';
import { useToast } from '../../composables/toast'; import { useToast } from '~/composables/toast';
import IconAddUsers from '~icons/lucide/plus'; import IconAddUsers from '~icons/lucide/plus';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast(); const toast = useToast();
// Get Users List // Get Users List
@@ -110,11 +113,11 @@ const createTeamLoading = ref(false);
const createTeam = async (newTeamName: string, ownerEmail: string) => { const createTeam = async (newTeamName: string, ownerEmail: string) => {
if (newTeamName.length < 6) { if (newTeamName.length < 6) {
toast.error('Team name should be atleast 6 characters long!!'); toast.error(`${t('state.team_name_long')}`);
return; return;
} }
if (ownerEmail.length == 0) { if (ownerEmail.length == 0) {
toast.error('Please enter email of team owner!!'); toast.error(`${t('state.enter_team_email')}`);
return; return;
} }
createTeamLoading.value = true; createTeamLoading.value = true;
@@ -124,13 +127,13 @@ const createTeam = async (newTeamName: string, ownerEmail: string) => {
await createTeamMutation.executeMutation(variables).then((result) => { await createTeamMutation.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
if (result.error.toString() == '[GraphQL] user/not_found') { if (result.error.toString() == '[GraphQL] user/not_found') {
toast.error('User not found!!'); toast.error(`${t('state.user_not_found')}`);
} else { } else {
toast.error('Failed to create team!!'); toast.error(`${t('state.create_team_failure')}`);
} }
createTeamLoading.value = false; createTeamLoading.value = false;
} else { } else {
toast.success('Team created successfully!!'); toast.success(`${t('state.create_team_success')}`);
showCreateTeamModal.value = false; showCreateTeamModal.value = false;
createTeamLoading.value = false; createTeamLoading.value = false;
refetch(); refetch();
@@ -164,16 +167,16 @@ const deleteTeam = (id: string) => {
const deleteTeamMutation = async (id: string | null) => { const deleteTeamMutation = async (id: string | null) => {
if (!id) { if (!id) {
confirmDeletion.value = false; confirmDeletion.value = false;
toast.error('Team deletion failed!!'); toast.error(`${t('state.delete_team_failure')}`);
return; return;
} }
const variables = { uid: id }; const variables = { uid: id };
await teamDeletion.executeMutation(variables).then((result) => { await teamDeletion.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('Team deletion failed!!'); toast.error(`${t('state.delete_team_failure')}`);
} else { } else {
teamList.value = teamList.value.filter((team) => team.id !== id); teamList.value = teamList.value.filter((team) => team.id !== id);
toast.success('Team deleted successfully!!'); toast.success(`${t('state.delete_team_success')}`);
} }
}); });
confirmDeletion.value = false; confirmDeletion.value = false;

View File

@@ -18,7 +18,7 @@
v-if="user.isAdmin" v-if="user.isAdmin"
class="absolute left-17 bottom-0 text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300" class="absolute left-17 bottom-0 text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300"
> >
Admin {{ t('users.admin') }}
</span> </span>
</div> </div>
@@ -28,12 +28,14 @@
v-if="user.isAdmin" v-if="user.isAdmin"
class="absolute left-15 bottom-0 text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300" class="absolute left-15 bottom-0 text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300"
> >
Admin {{ t('users.admin') }}
</span> </span>
</div> </div>
<div v-if="user.uid"> <div v-if="user.uid">
<label class="text-secondaryDark" for="username">UID</label> <label class="text-secondaryDark" for="username">{{
t('users.uid')
}}</label>
<div <div
class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500" class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
> >
@@ -41,18 +43,22 @@
</div> </div>
</div> </div>
<div> <div>
<label class="text-secondaryDark" for="username">Name</label> <label class="text-secondaryDark" for="username">{{
t('users.name')
}}</label>
<div <div
class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500" class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
> >
<span v-if="user.displayName"> <span v-if="user.displayName">
{{ user.displayName }} {{ user.displayName }}
</span> </span>
<span v-else> (Unnamed user) </span> <span v-else> {{ t('users.unnamed') }} </span>
</div> </div>
</div> </div>
<div v-if="user.email"> <div v-if="user.email">
<label class="text-secondaryDark" for="username">Email</label> <label class="text-secondaryDark" for="username">{{
t('users.email')
}}</label>
<div <div
class="w-full p-3 mt-2 bg-zinc-800 border-gray-200 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500" class="w-full p-3 mt-2 bg-zinc-800 border-gray-200 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
> >
@@ -60,7 +66,9 @@
</div> </div>
</div> </div>
<div v-if="user.createdOn"> <div v-if="user.createdOn">
<label class="text-secondaryDark" for="username">Created On</label> <label class="text-secondaryDark" for="username">{{
t('users.created_on')
}}</label>
<div <div
class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500" class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
> >
@@ -75,7 +83,7 @@
class="mr-4" class="mr-4"
filled filled
outline outline
label="Make Admin" :label="t('users.make_admin')"
@click="makeUserAdmin(user.uid)" @click="makeUserAdmin(user.uid)"
/> />
</span> </span>
@@ -85,7 +93,7 @@
filled filled
outline outline
:icon="IconUserMinus" :icon="IconUserMinus"
label="Remove Admin Privilege" :label="t('users.remove_admin_privilege')"
@click="makeAdminToUser(user.uid)" @click="makeAdminToUser(user.uid)"
/> />
</span> </span>
@@ -94,7 +102,7 @@
class="mr-4 !bg-red-600 !text-gray-300 !hover:text-gray-100" class="mr-4 !bg-red-600 !text-gray-300 !hover:text-gray-100"
filled filled
outline outline
label="Delete" :label="t('users.delete')"
:icon="IconTrash" :icon="IconTrash"
@click="deleteUser(user.uid)" @click="deleteUser(user.uid)"
/> />
@@ -105,26 +113,26 @@
filled filled
outline outline
:icon="IconTrash" :icon="IconTrash"
label="Delete" :label="t('users.delete')"
@click="toast.error('Remove admin privilege to delete the user!!')" @click="toast.error(t('state.remove_admin_to_delete_user'))"
/> />
</div> </div>
</div> </div>
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmDeletion" :show="confirmDeletion"
:title="`Confirm deletion of user?`" :title="t('users.confirm_user_deletion')"
@hide-modal="confirmDeletion = false" @hide-modal="confirmDeletion = false"
@resolve="deleteUserMutation(deleteUserUID)" @resolve="deleteUserMutation(deleteUserUID)"
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmUserToAdmin" :show="confirmUserToAdmin"
:title="`Do you want to make this user into an admin?`" :title="t('users.confirm_user_to_admin')"
@hide-modal="confirmUserToAdmin = false" @hide-modal="confirmUserToAdmin = false"
@resolve="makeUserAdminMutation(userToAdminUID)" @resolve="makeUserAdminMutation(userToAdminUID)"
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmAdminToUser" :show="confirmAdminToUser"
:title="`Do you want to remove admin status from this user?`" :title="t('users.confirm_admin_to_user')"
@hide-modal="confirmAdminToUser = false" @hide-modal="confirmAdminToUser = false"
@resolve="makeAdminToUserMutation(adminToUserUID)" @resolve="makeAdminToUserMutation(adminToUserUID)"
/> />
@@ -143,9 +151,12 @@ import {
import { useClientHandle } from '@urql/vue'; import { useClientHandle } from '@urql/vue';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useToast } from '../../composables/toast'; import { useToast } from '~/composables/toast';
import IconTrash from '~icons/lucide/trash'; import IconTrash from '~icons/lucide/trash';
import IconUserMinus from '~icons/lucide/user-minus'; import IconUserMinus from '~icons/lucide/user-minus';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast(); const toast = useToast();
@@ -166,7 +177,7 @@ onMounted(async () => {
.toPromise(); .toPromise();
if (result.error) { if (result.error) {
toast.error('Unable to load user info..'); toast.error(`${t('users.load_info_error')}`);
} }
user.value = result.data?.admin.userInfo ?? {}; user.value = result.data?.admin.userInfo ?? {};
fetching.value = false; fetching.value = false;
@@ -186,15 +197,15 @@ const deleteUser = (id: string) => {
const deleteUserMutation = async (id: string | null) => { const deleteUserMutation = async (id: string | null) => {
if (!id) { if (!id) {
confirmDeletion.value = false; confirmDeletion.value = false;
toast.error('User deletion failed!!'); toast.error(`${t('state.delete_user_failure')}`);
return; return;
} }
const variables = { uid: id }; const variables = { uid: id };
await userDeletion.executeMutation(variables).then((result) => { await userDeletion.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('User deletion failed!!'); toast.error(`${t('state.delete_user_failure')}`);
} else { } else {
toast.success('User deleted successfully!!'); toast.success(`${t('state.delete_user_success')}`);
} }
}); });
confirmDeletion.value = false; confirmDeletion.value = false;
@@ -215,16 +226,16 @@ const makeUserAdmin = (id: string) => {
const makeUserAdminMutation = async (id: string | null) => { const makeUserAdminMutation = async (id: string | null) => {
if (!id) { if (!id) {
confirmUserToAdmin.value = false; confirmUserToAdmin.value = false;
toast.error('User deletion failed!!'); toast.error(`${t('state.admin_failure')}`);
return; return;
} }
const variables = { uid: id }; const variables = { uid: id };
await userToAdmin.executeMutation(variables).then((result) => { await userToAdmin.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('Failed to make user an admin!!'); toast.error(`${t('state.admin_failure')}`);
} else { } else {
user.value.isAdmin = true; user.value.isAdmin = true;
toast.success('User is now an admin!!'); toast.success(`${t('state.admin_success')}`);
} }
}); });
confirmUserToAdmin.value = false; confirmUserToAdmin.value = false;
@@ -244,16 +255,16 @@ const makeAdminToUser = (id: string) => {
const makeAdminToUserMutation = async (id: string | null) => { const makeAdminToUserMutation = async (id: string | null) => {
if (!id) { if (!id) {
confirmAdminToUser.value = false; confirmAdminToUser.value = false;
toast.error('Failed to remove admin status!!'); toast.error(`${t('state.remove_admin_failure')}`);
return; return;
} }
const variables = { uid: id }; const variables = { uid: id };
await adminToUser.executeMutation(variables).then((result) => { await adminToUser.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('Failed to remove admin status!!'); toast.error(`${t('state.remove_admin_failure')}`);
} else { } else {
user.value.isAdmin = false; user.value.isAdmin = false;
toast.success('Admin status removed!!'); toast.error(`${t('state.remove_admin_success')}`);
} }
}); });
confirmAdminToUser.value = false; confirmAdminToUser.value = false;

View File

@@ -2,10 +2,12 @@
<div class="flex flex-col"> <div class="flex flex-col">
<!-- Table View for All Users --> <!-- Table View for All Users -->
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="text-lg font-bold text-secondaryDark">Users</h1> <h1 class="text-lg font-bold text-secondaryDark">
{{ t('users.users') }}
</h1>
<div class="flex items-center space-x-4 py-10"> <div class="flex items-center space-x-4 py-10">
<HoppButtonPrimary <HoppButtonPrimary
label="Invite a user" :label="t('users.invite_user')"
@click="showInviteUserModal = true" @click="showInviteUserModal = true"
:icon="IconAddUser" :icon="IconAddUser"
/> />
@@ -14,7 +16,7 @@
<HoppButtonSecondary <HoppButtonSecondary
outline outline
filled filled
label="Invited users" :label="t('users.invited_users')"
:to="'/users/invited'" :to="'/users/invited'"
/> />
</div> </div>
@@ -27,7 +29,7 @@
<HoppSmartSpinner /> <HoppSmartSpinner />
</div> </div>
<div v-else-if="error">Unable to Load Users List..</div> <div v-else-if="error">{{ t('users.load_list_error') }}</div>
<UsersTable <UsersTable
v-else-if="usersList.length >= 1" v-else-if="usersList.length >= 1"
@@ -40,14 +42,14 @@
@deleteUser="deleteUser" @deleteUser="deleteUser"
/> />
<div v-else class="flex justify-center">No Users Found</div> <div v-else class="flex justify-center">{{ t('users.no_users') }}</div>
<div <div
v-if="hasNextPage && usersList.length >= usersPerPage" v-if="hasNextPage && usersList.length >= usersPerPage"
class="flex justify-center my-5 px-3 py-2 cursor-pointer font-semibold rounded-3xl bg-dividerDark hover:bg-divider transition mx-auto w-38 text-secondaryDark" class="flex justify-center my-5 px-3 py-2 cursor-pointer font-semibold rounded-3xl bg-dividerDark hover:bg-divider transition mx-auto w-38 text-secondaryDark"
@click="fetchNextUsers" @click="fetchNextUsers"
> >
<span>Show more </span> <span>{{ t('users.show_more') }}</span>
<icon-lucide-chevron-down class="ml-2 text-lg" /> <icon-lucide-chevron-down class="ml-2 text-lg" />
</div> </div>
</div> </div>
@@ -60,19 +62,19 @@
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmDeletion" :show="confirmDeletion"
:title="`Confirm user deletion?`" :title="t('users.confirm_user_deletion')"
@hide-modal="confirmDeletion = false" @hide-modal="confirmDeletion = false"
@resolve="deleteUserMutation(deleteUserUID)" @resolve="deleteUserMutation(deleteUserUID)"
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmUserToAdmin" :show="confirmUserToAdmin"
:title="`Do you want to make this user into an admin?`" :title="t('users.confirm_user_to_admin')"
@hide-modal="confirmUserToAdmin = false" @hide-modal="confirmUserToAdmin = false"
@resolve="makeUserAdminMutation(userToAdminUID)" @resolve="makeUserAdminMutation(userToAdminUID)"
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmAdminToUser" :show="confirmAdminToUser"
:title="`Do you want to remove admin status from this user?`" :title="t('users.confirm_admin_to_user')"
@hide-modal="confirmAdminToUser = false" @hide-modal="confirmAdminToUser = false"
@resolve="makeAdminToUserMutation(adminToUserUID)" @resolve="makeAdminToUserMutation(adminToUserUID)"
/> />
@@ -89,11 +91,14 @@ import {
RemoveUserAsAdminDocument, RemoveUserAsAdminDocument,
UsersListDocument, UsersListDocument,
} from '../../helpers/backend/graphql'; } from '../../helpers/backend/graphql';
import { usePagedQuery } from '../../composables/usePagedQuery'; import { usePagedQuery } from '~/composables/usePagedQuery';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useToast } from '../../composables/toast'; import { useToast } from '~/composables/toast';
import { HoppButtonSecondary } from '@hoppscotch/ui'; import { HoppButtonSecondary } from '@hoppscotch/ui';
import IconAddUser from '~icons/lucide/user-plus'; import IconAddUser from '~icons/lucide/user-plus';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast(); const toast = useToast();
@@ -119,15 +124,15 @@ const showInviteUserModal = ref(false);
const sendInvite = async (email: string) => { const sendInvite = async (email: string) => {
if (!email.trim()) { if (!email.trim()) {
toast.error('Please enter a valid email address'); toast.error(`${t('state.invalid_email')}`);
return; return;
} }
const variables = { inviteeEmail: email.trim() }; const variables = { inviteeEmail: email.trim() };
await sendInvitation.executeMutation(variables).then((result) => { await sendInvitation.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('Failed to send invitation'); toast.error(`${t('state.email_failure')}`);
} else { } else {
toast.success('Email invitation sent successfully'); toast.success(`${t('state.email_success')}`);
showInviteUserModal.value = false; showInviteUserModal.value = false;
} }
}); });
@@ -153,15 +158,15 @@ const deleteUserUID = ref<string | null>(null);
const deleteUserMutation = async (id: string | null) => { const deleteUserMutation = async (id: string | null) => {
if (!id) { if (!id) {
confirmDeletion.value = false; confirmDeletion.value = false;
toast.error('User deletion failed!!'); toast.error(`${t('state.delete_user_failure')}`);
return; return;
} }
const variables = { uid: id }; const variables = { uid: id };
await userDeletion.executeMutation(variables).then((result) => { await userDeletion.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('User deletion failed!!'); toast.error(`${t('state.delete_user_failure')}`);
} else { } else {
toast.success('User deleted successfully!!'); toast.success(`${t('state.delete_user_success')}`);
usersList.value = usersList.value.filter((user) => user.uid !== id); usersList.value = usersList.value.filter((user) => user.uid !== id);
} }
}); });
@@ -182,15 +187,15 @@ const makeUserAdmin = (id: string) => {
const makeUserAdminMutation = async (id: string | null) => { const makeUserAdminMutation = async (id: string | null) => {
if (!id) { if (!id) {
confirmUserToAdmin.value = false; confirmUserToAdmin.value = false;
toast.error('Failed to make user an admin!!'); toast.error(`${t('state.admin_failure')}`);
return; return;
} }
const variables = { uid: id }; const variables = { uid: id };
await userToAdmin.executeMutation(variables).then((result) => { await userToAdmin.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('Failed to make user an admin!!'); toast.error(`${t('state.admin_failure')}`);
} else { } else {
toast.success('User is now an admin!!'); toast.success(`${t('state.admin_success')}`);
usersList.value = usersList.value.map((user) => { usersList.value = usersList.value.map((user) => {
if (user.uid === id) { if (user.uid === id) {
user.isAdmin = true; user.isAdmin = true;
@@ -221,15 +226,15 @@ const deleteUser = (id: string) => {
const makeAdminToUserMutation = async (id: string | null) => { const makeAdminToUserMutation = async (id: string | null) => {
if (!id) { if (!id) {
confirmAdminToUser.value = false; confirmAdminToUser.value = false;
toast.error('Failed to remove admin status!!'); toast.error(`${t('state.remove_admin_failure')}`);
return; return;
} }
const variables = { uid: id }; const variables = { uid: id };
await adminToUser.executeMutation(variables).then((result) => { await adminToUser.executeMutation(variables).then((result) => {
if (result.error) { if (result.error) {
toast.error('Failed to remove admin status!!'); toast.error(`${t('state.remove_admin_failure')}`);
} else { } else {
toast.success('Admin status removed!!'); toast.success(`${t('state.remove_admin_success')}`);
usersList.value = usersList.value.map((user) => { usersList.value = usersList.value.map((user) => {
if (user.uid === id) { if (user.uid === id) {
user.isAdmin = false; user.isAdmin = false;

View File

@@ -6,7 +6,9 @@
</button> </button>
</div> </div>
<h3 class="text-lg font-bold text-accentContrast py-6">Invited Users</h3> <h3 class="text-lg font-bold text-accentContrast py-6">
{{ t('users.invited_users') }}
</h3>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="py-2 overflow-x-auto"> <div class="py-2 overflow-x-auto">
@@ -15,7 +17,7 @@
<HoppSmartSpinner /> <HoppSmartSpinner />
</div> </div>
<div v-else-if="error || invitedUsers === undefined"> <div v-else-if="error || invitedUsers === undefined">
<p class="text-xl">No invited users found..</p> <p class="text-xl">{{ t('users.no_invite') }}</p>
</div> </div>
<table v-else class="w-full text-left"> <table v-else class="w-full text-left">
@@ -23,10 +25,10 @@
<tr <tr
class="text-secondary border-b border-dividerDark text-sm text-left" class="text-secondary border-b border-dividerDark text-sm text-left"
> >
<th class="px-3 pb-3">Admin ID</th> <th class="px-3 pb-3">{{ t('users.admin_id') }}</th>
<th class="px-3 pb-3">Admin Email</th> <th class="px-3 pb-3">{{ t('users.admin_email') }}</th>
<th class="px-3 pb-3">Invitee Email</th> <th class="px-3 pb-3">{{ t('users.invitee_email') }}</th>
<th class="px-3 pb-3">Invited On</th> <th class="px-3 pb-3">{{ t('users.invited_on') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-divider"> <tbody class="divide-y divide-divider">
@@ -34,7 +36,7 @@
v-if="invitedUsers.length === 0" v-if="invitedUsers.length === 0"
class="text-secondaryDark py-4" class="text-secondaryDark py-4"
> >
<div class="py-6 px-3">No invited users found..</div> <div class="py-6 px-3">{{ t('users.no_invite') }}</div>
</tr> </tr>
<tr <tr
v-else v-else
@@ -85,6 +87,9 @@ import { InvitedUsersDocument } from '../../helpers/backend/graphql';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { HoppSmartSpinner } from '@hoppscotch/ui'; import { HoppSmartSpinner } from '@hoppscotch/ui';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const router = useRouter(); const router = useRouter();

View File

@@ -7,6 +7,7 @@ import Components from 'unplugin-vue-components/vite';
import WindiCSS from 'vite-plugin-windicss'; import WindiCSS from 'vite-plugin-windicss';
import Pages from 'vite-plugin-pages'; import Pages from 'vite-plugin-pages';
import Layouts from 'vite-plugin-vue-layouts'; import Layouts from 'vite-plugin-vue-layouts';
import VueI18n from '@intlify/vite-plugin-vue-i18n';
import path from 'path'; import path from 'path';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
@@ -31,6 +32,11 @@ export default defineConfig({
defaultLayout: 'default', defaultLayout: 'default',
layoutsDirs: 'src/layouts', layoutsDirs: 'src/layouts',
}), }),
VueI18n({
runtimeOnly: false,
compositionOnly: true,
include: [path.resolve(__dirname, 'locales')],
}),
WindiCSS({ WindiCSS({
root: path.resolve(__dirname), root: path.resolve(__dirname),
}), }),

64
pnpm-lock.yaml generated
View File

@@ -917,7 +917,7 @@ importers:
version: 3.1.1(graphql@15.8.0) version: 3.1.1(graphql@15.8.0)
'@intlify/vite-plugin-vue-i18n': '@intlify/vite-plugin-vue-i18n':
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0(vite@3.1.4)(vue-i18n@9.2.2) version: 7.0.0(vite@3.2.4)(vue-i18n@9.2.2)
'@rushstack/eslint-patch': '@rushstack/eslint-patch':
specifier: ^1.1.4 specifier: ^1.1.4
version: 1.1.4 version: 1.1.4
@@ -1068,6 +1068,9 @@ importers:
vue: vue:
specifier: ^3.2.6 specifier: ^3.2.6
version: 3.2.45 version: 3.2.45
vue-i18n:
specifier: ^9.2.2
version: 9.2.2(vue@3.2.45)
vue-router: vue-router:
specifier: '4' specifier: '4'
version: 4.1.0(vue@3.2.45) version: 4.1.0(vue@3.2.45)
@@ -1099,6 +1102,9 @@ importers:
'@graphql-codegen/urql-introspection': '@graphql-codegen/urql-introspection':
specifier: 2.2.1 specifier: 2.2.1
version: 2.2.1(graphql@16.6.0) version: 2.2.1(graphql@16.6.0)
'@intlify/vite-plugin-vue-i18n':
specifier: ^7.0.0
version: 7.0.0(vite@3.2.4)(vue-i18n@9.2.2)
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.2.0(vite@3.2.4)(vue@3.2.45) version: 3.2.0(vite@3.2.4)(vue@3.2.45)
@@ -5765,6 +5771,34 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@intlify/vite-plugin-vue-i18n@7.0.0(vite@3.2.4)(vue-i18n@9.2.2):
resolution: {integrity: sha512-2TbDOQ8XD+vkc0s5OFmr+IY/k4mYMC7pzvx0xGQn+cU/ev314+yi7Z7N7rWcBgiYk1WOUalbGSo3d4nJDxOOyw==}
engines: {node: '>= 14.6'}
deprecated: This plugin support until Vite 3. If you would like to use on Vite 4, please use @intlify/unplugin-vue-i18n
peerDependencies:
petite-vue-i18n: '*'
vite: ^2.9.0 || ^3.0.0
vue-i18n: '*'
peerDependenciesMeta:
petite-vue-i18n:
optional: true
vite:
optional: true
vue-i18n:
optional: true
dependencies:
'@intlify/bundle-utils': 3.4.0(vue-i18n@9.2.2)
'@intlify/shared': 9.3.0-beta.17
'@rollup/pluginutils': 4.2.1
debug: 4.3.4(supports-color@9.2.2)
fast-glob: 3.2.12
source-map: 0.6.1
vite: 3.2.4(@types/node@17.0.45)(sass@1.53.0)(terser@5.14.1)
vue-i18n: 9.2.2(vue@3.2.45)
transitivePeerDependencies:
- supports-color
dev: true
/@intlify/vue-devtools@9.2.2: /@intlify/vue-devtools@9.2.2:
resolution: {integrity: sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==} resolution: {integrity: sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
@@ -9169,7 +9203,7 @@ packages:
hasBin: true hasBin: true
/after@0.8.2: /after@0.8.2:
resolution: {integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=} resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==}
dev: false dev: false
/agent-base@6.0.2: /agent-base@6.0.2:
@@ -9781,7 +9815,7 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
/base64-arraybuffer@0.1.4: /base64-arraybuffer@0.1.4:
resolution: {integrity: sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=} resolution: {integrity: sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==}
engines: {node: '>= 0.6.0'} engines: {node: '>= 0.6.0'}
dev: false dev: false
@@ -10428,14 +10462,14 @@ packages:
dev: true dev: true
/component-bind@1.0.0: /component-bind@1.0.0:
resolution: {integrity: sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=} resolution: {integrity: sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==}
dev: false dev: false
/component-emitter@1.3.0: /component-emitter@1.3.0:
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
/component-inherit@0.0.3: /component-inherit@0.0.3:
resolution: {integrity: sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=} resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==}
dev: false dev: false
/concat-map@0.0.1: /concat-map@0.0.1:
@@ -13635,7 +13669,7 @@ packages:
dev: false dev: false
/has-cors@1.1.0: /has-cors@1.1.0:
resolution: {integrity: sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=} resolution: {integrity: sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==}
dev: false dev: false
/has-flag@3.0.0: /has-flag@3.0.0:
@@ -14028,7 +14062,7 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
/indexof@0.0.1: /indexof@0.0.1:
resolution: {integrity: sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=} resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
dev: false dev: false
/inflight@1.0.6: /inflight@1.0.6:
@@ -19743,7 +19777,7 @@ packages:
dev: true dev: true
/to-array@0.1.4: /to-array@0.1.4:
resolution: {integrity: sha1-F+bBH3PdTz10zaek/zI46a2b+JA=} resolution: {integrity: sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==}
dev: false dev: false
/to-fast-properties@2.0.0: /to-fast-properties@2.0.0:
@@ -21458,6 +21492,18 @@ packages:
'@vue/devtools-api': 6.2.1 '@vue/devtools-api': 6.2.1
vue: 3.2.37 vue: 3.2.37
/vue-i18n@9.2.2(vue@3.2.45):
resolution: {integrity: sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==}
engines: {node: '>= 14'}
peerDependencies:
vue: ^3.0.0
dependencies:
'@intlify/core-base': 9.2.2
'@intlify/shared': 9.2.2
'@intlify/vue-devtools': 9.2.2
'@vue/devtools-api': 6.2.1
vue: 3.2.45
/vue-loader@16.8.3(@vue/compiler-sfc@3.2.45)(vue@3.2.45)(webpack@5.74.0): /vue-loader@16.8.3(@vue/compiler-sfc@3.2.45)(vue@3.2.45)(webpack@5.74.0):
resolution: {integrity: sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==} resolution: {integrity: sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==}
peerDependencies: peerDependencies:
@@ -22358,7 +22404,7 @@ packages:
dev: false dev: false
/yeast@0.1.2: /yeast@0.1.2:
resolution: {integrity: sha1-AI4G2AlDIMNy28L47XagymyKxBk=} resolution: {integrity: sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==}
dev: false dev: false
/yn@3.1.1: /yn@3.1.1: