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",
"lodash-es": "^4.17.21",
"rxjs": "^7.8.0",
"tippy.js": "^6.3.7",
"ts-node-dev": "^2.0.0",
"unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.21.0",
"vue": "^3.2.6",
"vue-i18n": "^9.2.2",
"vue-router": "4",
"tippy.js": "^6.3.7",
"vue-tippy": "6.0.0-alpha.58"
},
"devDependencies": {
@@ -47,6 +48,7 @@
"@graphql-codegen/typescript-document-nodes": "3.0.0",
"@graphql-codegen/typescript-operations": "3.0.0",
"@graphql-codegen/urql-introspection": "2.2.1",
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@vitejs/plugin-vue": "^3.1.0",
"@vue/compiler-sfc": "^3.2.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']
AppToast: typeof import('./components/app/Toast.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']
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']

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'];

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>
<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">
<HoppSmartSpinner />
</div>
<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 v-else>
<div class="py-10 grid lg:grid-cols-2 gap-6">
<DashboardMetricsCard
:count="metrics.usersCount"
label="Total Users"
:label="t('metrics.total_users')"
:icon="UserIcon"
color="text-green-400"
/>
<DashboardMetricsCard
:count="metrics.teamsCount"
label="Total Teams"
:label="t('metrics.total_teams')"
:icon="UsersIcon"
color="text-pink-400"
/>
<DashboardMetricsCard
:count="metrics.teamRequestsCount"
label="Total Requests"
:label="t('metrics.total_requests')"
:icon="LineChartIcon"
color="text-cyan-400"
/>
<DashboardMetricsCard
:count="metrics.teamCollectionsCount"
label="Total Collections"
:label="t('metrics.total_collections')"
:icon="FolderTreeIcon"
color="text-orange-400"
/>
@@ -49,6 +51,9 @@ import UserIcon from '~icons/lucide/user';
import UsersIcon from '~icons/lucide/users';
import LineChartIcon from '~icons/lucide/line-chart';
import FolderTreeIcon from '~icons/lucide/folder-tree';
import { useI18n } from '../composables/i18n';
const t = useI18n();
// Get Metrics Data
const { fetching, error, data } = useQuery({ query: MetricsDocument });

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,9 @@
</button>
</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="py-2 overflow-x-auto">
@@ -15,7 +17,7 @@
<HoppSmartSpinner />
</div>
<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>
<table v-else class="w-full text-left">
@@ -23,10 +25,10 @@
<tr
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">Admin Email</th>
<th class="px-3 pb-3">Invitee Email</th>
<th class="px-3 pb-3">Invited On</th>
<th class="px-3 pb-3">{{ t('users.admin_id') }}</th>
<th class="px-3 pb-3">{{ t('users.admin_email') }}</th>
<th class="px-3 pb-3">{{ t('users.invitee_email') }}</th>
<th class="px-3 pb-3">{{ t('users.invited_on') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-divider">
@@ -34,7 +36,7 @@
v-if="invitedUsers.length === 0"
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
v-else
@@ -85,6 +87,9 @@ import { InvitedUsersDocument } from '../../helpers/backend/graphql';
import { format } from 'date-fns';
import { HoppSmartSpinner } from '@hoppscotch/ui';
import { useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const router = useRouter();

View File

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