feat: introducing i18n support to admin dashboard (#3051)
This commit is contained in:
committed by
GitHub
parent
b07243f131
commit
331d482b22
1
packages/hoppscotch-sh-admin/.dockerignore
Normal file
1
packages/hoppscotch-sh-admin/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
./node_modules
|
||||
195
packages/hoppscotch-sh-admin/languages.json
Normal file
195
packages/hoppscotch-sh-admin/languages.json
Normal 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"
|
||||
}
|
||||
]
|
||||
3
packages/hoppscotch-sh-admin/locales/af.json
Normal file
3
packages/hoppscotch-sh-admin/locales/af.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/ar.json
Normal file
3
packages/hoppscotch-sh-admin/locales/ar.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/ca.json
Normal file
3
packages/hoppscotch-sh-admin/locales/ca.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/cn.json
Normal file
3
packages/hoppscotch-sh-admin/locales/cn.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/cs.json
Normal file
3
packages/hoppscotch-sh-admin/locales/cs.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/da.json
Normal file
3
packages/hoppscotch-sh-admin/locales/da.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/de.json
Normal file
3
packages/hoppscotch-sh-admin/locales/de.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/el.json
Normal file
3
packages/hoppscotch-sh-admin/locales/el.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
132
packages/hoppscotch-sh-admin/locales/en.json
Normal file
132
packages/hoppscotch-sh-admin/locales/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/es.json
Normal file
3
packages/hoppscotch-sh-admin/locales/es.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/fi.json
Normal file
3
packages/hoppscotch-sh-admin/locales/fi.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/fr.json
Normal file
3
packages/hoppscotch-sh-admin/locales/fr.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/he.json
Normal file
3
packages/hoppscotch-sh-admin/locales/he.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/hi.json
Normal file
3
packages/hoppscotch-sh-admin/locales/hi.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/hu.json
Normal file
3
packages/hoppscotch-sh-admin/locales/hu.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/id.json
Normal file
3
packages/hoppscotch-sh-admin/locales/id.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/it.json
Normal file
3
packages/hoppscotch-sh-admin/locales/it.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/ja.json
Normal file
3
packages/hoppscotch-sh-admin/locales/ja.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/ko.json
Normal file
3
packages/hoppscotch-sh-admin/locales/ko.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/nl.json
Normal file
3
packages/hoppscotch-sh-admin/locales/nl.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/no.json
Normal file
3
packages/hoppscotch-sh-admin/locales/no.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/pl.json
Normal file
3
packages/hoppscotch-sh-admin/locales/pl.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/pt-br.json
Normal file
3
packages/hoppscotch-sh-admin/locales/pt-br.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/pt.json
Normal file
3
packages/hoppscotch-sh-admin/locales/pt.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/ro.json
Normal file
3
packages/hoppscotch-sh-admin/locales/ro.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/ru.json
Normal file
3
packages/hoppscotch-sh-admin/locales/ru.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/sr.json
Normal file
3
packages/hoppscotch-sh-admin/locales/sr.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/sv.json
Normal file
3
packages/hoppscotch-sh-admin/locales/sv.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/tr.json
Normal file
3
packages/hoppscotch-sh-admin/locales/tr.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/tw.json
Normal file
3
packages/hoppscotch-sh-admin/locales/tw.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/uk.json
Normal file
3
packages/hoppscotch-sh-admin/locales/uk.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
3
packages/hoppscotch-sh-admin/locales/vi.json
Normal file
3
packages/hoppscotch-sh-admin/locales/vi.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'];
|
||||
|
||||
6
packages/hoppscotch-sh-admin/src/composables/i18n.ts
Normal file
6
packages/hoppscotch-sh-admin/src/composables/i18n.ts
Normal 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
|
||||
3
packages/hoppscotch-sh-admin/src/helpers/error.ts
Normal file
3
packages/hoppscotch-sh-admin/src/helpers/error.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const throwError = (message: string): never => {
|
||||
throw new Error(message)
|
||||
}
|
||||
154
packages/hoppscotch-sh-admin/src/modules/i18n.ts
Normal file
154
packages/hoppscotch-sh-admin/src/modules/i18n.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user