refactor: consolidated admin dashboard improvements (#3790)

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
Joel Jacob Stephen
2024-02-02 15:17:25 +05:30
committed by GitHub
parent aab76f1358
commit 3d6adcc39d
20 changed files with 763 additions and 716 deletions

View File

@@ -1,51 +1,54 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
import '@vue/runtime-core';
export {}
export {};
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AppHeader: typeof import('./components/app/Header.vue')['default']
AppLogin: typeof import('./components/app/Login.vue')['default']
AppLogout: typeof import('./components/app/Logout.vue')['default']
AppModal: typeof import('./components/app/Modal.vue')['default']
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']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default']
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default']
SettingsReset: typeof import('./components/settings/Reset.vue')['default']
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default']
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.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']
TeamsMembers: typeof import('./components/teams/Members.vue')['default']
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default']
Tippy: typeof import('vue-tippy')['Tippy']
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default']
UsersDetails: typeof import('./components/users/Details.vue')['default']
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default'];
AppLogin: typeof import('./components/app/Login.vue')['default'];
AppLogout: typeof import('./components/app/Logout.vue')['default'];
AppModal: typeof import('./components/app/Modal.vue')['default'];
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'];
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete'];
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'];
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder'];
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper'];
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab'];
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable'];
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs'];
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle'];
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'];
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'];
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default'];
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'];
IconLucideUser: typeof import('~icons/lucide/user')['default'];
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default'];
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default'];
SettingsReset: typeof import('./components/settings/Reset.vue')['default'];
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default'];
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.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'];
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
Tippy: typeof import('vue-tippy')['Tippy'];
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default'];
UsersDetails: typeof import('./components/users/Details.vue')['default'];
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default'];
}
}

View File

@@ -72,13 +72,14 @@ onMounted(async () => {
success = await resetInfraConfigs(resetInfraConfigsMutation);
if (!success) return;
} else {
const infraResult = await updateInfraConfigs(updateInfraConfigsMutation);
if (!infraResult) return;
const authResult = await updateAuthProvider(
updateAllowedAuthProviderMutation
);
const infraResult = await updateInfraConfigs(updateInfraConfigsMutation);
success = authResult && infraResult;
if (!success) return;
if (!authResult) return;
}
restart.value = true;

View File

@@ -3,7 +3,7 @@
v-if="show"
dialog
:title="t('teams.create_team')"
@close="$emit('hide-modal')"
@close="emit('hide-modal')"
>
<template #body>
<div class="flex flex-col space-y-4 relative">
@@ -16,12 +16,16 @@
class="flex-1 !flex"
:source="allUsersEmail"
:spellcheck="true"
placeholder=""
:placeholder="t('teams.email')"
@input="(email: string) => getOwnerEmail(email)"
/>
</div>
<label for="teamName"> {{ t('teams.name') }} </label>
<HoppSmartInput v-model="teamName" placeholder="" class="!my-2" />
<HoppSmartInput
v-model="teamName"
:placeholder="t('teams.name')"
class="!my-2"
/>
</div>
</template>
<template #footer>
@@ -44,11 +48,10 @@
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import { useToast } from '~/composables/toast';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
const t = useI18n();
const toast = useToast();
const props = withDefaults(
@@ -82,11 +85,11 @@ const getOwnerEmail = (email: string) => (ownerEmail.value = email);
const createTeam = () => {
if (teamName.value.trim() === '') {
toast.error(`${t('teams.valid_name')}`);
toast.error(t('teams.valid_name'));
return;
}
if (ownerEmail.value.trim() === '') {
toast.error(`${t('teams.valid_owner_email')}`);
toast.error(t('teams.valid_owner_email'));
return;
}
emit('create-team', teamName.value, ownerEmail.value);

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col">
<div v-if="team" class="flex flex-col">
<div class="flex flex-col space-y-8">
<div v-if="team.id" class="flex flex-col space-y-3">
<div class="flex flex-col space-y-3">
<label class="text-accentContrast" for="username"
>{{ t('teams.id') }}
</label>
@@ -10,33 +10,33 @@
</div>
</div>
<div v-if="teamName" class="flex flex-col space-y-3">
<div class="flex flex-col space-y-3">
<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="{
'!border-accent': showRenameInput,
'!border-accent': isTeamNameBeingEdited,
}"
>
<HoppSmartInput
v-model="newTeamName"
v-model="updatedTeamName"
styles="bg-transparent flex-1 rounded-md !rounded-r-none disabled:select-none border-r-0 disabled:cursor-default disabled:opacity-50"
placeholder="Team Name"
:disabled="!showRenameInput"
:disabled="!isTeamNameBeingEdited"
>
<template #button>
<HoppButtonPrimary
class="!rounded-l-none"
filled
:icon="showRenameInput ? IconSave : IconEdit"
:icon="isTeamNameBeingEdited ? IconSave : IconEdit"
:label="
showRenameInput
isTeamNameBeingEdited
? `${t('teams.rename')}`
: `${t('teams.edit')}`
"
@click="handleNameEdit()"
@click="handleTeamNameEdit"
/>
</template>
</HoppSmartInput>
@@ -58,7 +58,7 @@
class="!bg-red-600 !hover:opacity-80"
filled
:label="t('teams.delete_team')"
@click="team && $emit('delete-team', team.id)"
@click="emit('delete-team', team.id)"
:icon="IconTrash"
/>
</div>
@@ -66,45 +66,89 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useMutation } from '@urql/vue';
import { useVModel } from '@vueuse/core';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { TeamInfoQuery } from '~/helpers/backend/graphql';
import { RenameTeamDocument, 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();
const props = defineProps<{
team: TeamInfoQuery['infra']['teamInfo'];
teamName: string;
showRenameInput: boolean;
}>();
const emit = defineEmits<{
(event: 'delete-team', teamID: string): void;
(event: 'rename-team', teamName: string): void;
(event: 'update:showRenameInput', showRenameInput: boolean): void;
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
(event: 'delete-team', teamId: string): void;
}>();
const newTeamName = ref(props.teamName);
const team = useVModel(props, 'team', emit);
const handleNameEdit = () => {
if (props.showRenameInput) {
renameTeam();
// Contains the actual team name
const teamName = computed({
get: () => team.value.name,
set: (value) => {
team.value.name = value;
},
});
// Contains the stored team name from the actual team name before being edited
const currentTeamName = ref('');
// Contains the team name that is being edited
const updatedTeamName = computed({
get: () => currentTeamName.value,
set: (value) => {
currentTeamName.value = value;
},
});
// Set the current team name to the actual team name
onMounted(() => {
currentTeamName.value = teamName.value;
});
// Rename the team name
const isTeamNameBeingEdited = ref(false);
const teamRename = useMutation(RenameTeamDocument);
const handleTeamNameEdit = () => {
if (isTeamNameBeingEdited.value) {
// If the team name is not changed, then return control
if (teamName.value !== updatedTeamName.value) {
renameTeamName();
} else isTeamNameBeingEdited.value = false;
} else {
emit('update:showRenameInput', true);
isTeamNameBeingEdited.value = true;
}
};
const renameTeam = () => {
if (newTeamName.value.trim() === '') {
toast.error(`${t('teams.empty_name')}`);
const renameTeamName = async () => {
if (updatedTeamName.value.trim() === '') {
toast.error(t('teams.empty_name'));
return;
}
emit('rename-team', newTeamName.value);
if (updatedTeamName.value.length < 6) {
toast.error(t('state.team_name_too_short'));
return;
}
const variables = { uid: team.value.id, name: updatedTeamName.value };
const result = await teamRename.executeMutation(variables);
if (result.error) {
toast.error(t('state.rename_team_failure'));
} else {
isTeamNameBeingEdited.value = false;
toast.success(t('state.rename_team_success'));
teamName.value = updatedTeamName.value;
}
};
</script>

View File

@@ -1,16 +1,23 @@
<template>
<HoppSmartModal v-if="show" dialog title="Add Member" @close="hideModal">
<HoppSmartModal
v-if="show"
dialog
:title="t('teams.add_member')"
@close="hideModal"
>
<template #body>
<div v-if="addingUserToTeam" class="flex items-center justify-center p-4">
<HoppSmartSpinner />
</div>
<div v-else class="flex flex-col">
<div class="flex items-center justify-between flex-1 pt-4">
<label for="memberList" class="p-4"> Add members </label>
<label for="memberList" class="p-4">
{{ t('teams.add_members') }}
</label>
<div class="flex">
<HoppButtonSecondary
:icon="IconPlus"
label="Add new"
:label="t('teams.add_new')"
filled
@click="addNewMember"
/>
@@ -23,8 +30,8 @@
class="flex divide-x divide-dividerLight"
>
<HoppSmartAutoComplete
v-model="member.key"
placeholder="Email"
:value="member.key"
:placeholder="t('state.email')"
:source="allUsersEmail"
:name="'member' + index"
:spellcheck="true"
@@ -44,7 +51,7 @@
<HoppSmartSelectWrapper>
<input
class="flex flex-1 px-4 py-2 bg-transparent cursor-pointer"
placeholder="Permissions"
:placeholder="t('teams.permissions')"
:name="'value' + index"
:value="member.value"
readonly
@@ -58,7 +65,7 @@
@keyup.escape="hide()"
>
<HoppSmartItem
label="OWNER"
:label="t('teams.owner')"
:icon="
member.value === 'OWNER' ? IconCircleDot : IconCircle
"
@@ -71,7 +78,7 @@
"
/>
<HoppSmartItem
label="EDITOR"
:label="t('teams.editor')"
:icon="
member.value === 'EDITOR' ? IconCircleDot : IconCircle
"
@@ -84,7 +91,7 @@
"
/>
<HoppSmartItem
label="VIEWER"
:label="t('teams.viewer')"
:icon="
member.value === 'VIEWER' ? IconCircleDot : IconCircle
"
@@ -104,7 +111,7 @@
<HoppButtonSecondary
id="member"
v-tippy="{ theme: 'tooltip' }"
title="Remove"
:title="t('teams.remove')"
:icon="IconTrash"
color="red"
@click="removeNewMember(index)"
@@ -113,13 +120,13 @@
</div>
<HoppSmartPlaceholder
v-if="newMembersList.length === 0"
:src="`/images/states/dark/add_group.svg`"
alt="No invites"
text="No invites"
:src="addGroupImagePath"
:alt="t('teams.no_members')"
:text="t('teams.no_members')"
>
<template #body>
<HoppButtonSecondary
label="Add new"
:label="t('teams.add_new')"
filled
@click="addNewMember"
/>
@@ -136,11 +143,12 @@
<icon-lucide-help-circle
class="mr-2 text-secondaryLight svg-icons"
/>
Roles
{{ t('teams.roles') }}
</span>
<p>
<span class="text-secondaryLight">
Roles are used to control access to the shared collections.
{{ t('teams.roles_description') }}
</span>
</p>
<ul class="mt-4 space-y-4">
@@ -148,31 +156,30 @@
<span
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
>
Owner
{{ t('teams.owner') }}
</span>
<span class="flex flex-1">
Owners can add, edit, and delete requests, collections and team
members.
{{ t('teams.owner_description') }}
</span>
</li>
<li class="flex">
<span
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
>
Editor
{{ t('teams.editor') }}
</span>
<span class="flex flex-1">
Editors can add, edit, and delete requests.
{{ t('teams.editor_description') }}
</span>
</li>
<li class="flex">
<span
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
>
Viewer
{{ t('teams.viewer') }}
</span>
<span class="flex flex-1">
Viewers can only view and use requests.
{{ t('teams.viewer_description') }}
</span>
</li>
</ul>
@@ -183,41 +190,61 @@
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
label="Add Member"
:label="t('teams.add_member')"
outline
@click="addUserasTeamMember"
/>
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
<HoppButtonSecondary
:label="t('teams.cancel')"
outline
filled
@click="hideModal"
/>
</span>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useMutation, useQuery } from '@urql/vue';
import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
import { computed, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { usePagedQuery } from '~/composables/usePagedQuery';
import { Email, EmailCodec } from '~/helpers/Email';
import IconCircle from '~icons/lucide/circle';
import IconCircleDot from '~icons/lucide/circle-dot';
import IconPlus from '~icons/lucide/plus';
import IconTrash from '~icons/lucide/trash';
import {
AddUserToTeamByAdminDocument,
TeamMemberRole,
MetricsDocument,
TeamMemberRole,
UsersListDocument,
} from '../../helpers/backend/graphql';
import { useToast } from '~/composables/toast';
import { useMutation, useQuery } from '@urql/vue';
import { Email, EmailCodec } from '~/helpers/Email';
import IconTrash from '~icons/lucide/trash';
import IconPlus from '~icons/lucide/plus';
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();
const toast = useToast();
const tippyActions = ref<any | null>(null);
// Get Users List
const props = defineProps<{
show: boolean;
editingTeamID: string;
}>();
const emit = defineEmits<{
(e: 'hide-modal'): void;
(e: 'member'): void;
}>();
const addGroupImagePath = `${
import.meta.env.VITE_ADMIN_URL
}/assets/images/add_group.svg`;
// Get Users List to extract email ids of all users
const { data } = useQuery({ query: MetricsDocument });
const usersPerPage = computed(() => data.value?.infra.usersCount || 10000);
@@ -231,21 +258,6 @@ const { list: usersList } = usePagedQuery(
const allUsersEmail = computed(() => usersList.value.map((user) => user.email));
const toast = useToast();
// Template refs
const tippyActions = ref<any | null>(null);
const props = defineProps({
show: Boolean,
editingTeamID: { type: String, default: null },
});
const emit = defineEmits<{
(e: 'hide-modal'): void;
(e: 'member'): void;
}>();
const newMembersList = ref<Array<{ key: string; value: TeamMemberRole }>>([
{
key: '',
@@ -264,12 +276,13 @@ const updateNewMemberRole = (index: number, role: TeamMemberRole) => {
newMembersList.value[index].value = role;
};
const removeNewMember = (id: number) => {
newMembersList.value.splice(id, 1);
const removeNewMember = (index: number) => {
newMembersList.value.splice(index, 1);
};
const addingUserToTeam = ref<boolean>(false);
// Checks if the member invites are of valid email format and then adds the users to the team
const addUserasTeamMember = async () => {
addingUserToTeam.value = true;
@@ -293,7 +306,7 @@ const addUserasTeamMember = async () => {
if (O.isNone(validationResult)) {
// Error handling for no validation
toast.error(`${t('users.invalid_user')}`);
toast.error(t('users.invalid_user'));
addingUserToTeam.value = false;
return;
}
@@ -320,20 +333,18 @@ const addUserToTeam = async (
) => {
const variables = { userEmail: email, role: userRole, teamID: teamID };
const res = await addUserToTeamMutation
.executeMutation(variables)
.then((result) => {
if (result.error) {
if (result.error.toString() == '[GraphQL] user/not_found') {
toast.error(`${t('state.user_not_found')}`);
} else {
toast.error(`${t('state.add_user_failure')}`);
}
} else {
toast.success(`${t('state.add_user_success')}`);
emit('member');
}
});
return res;
const result = await addUserToTeamMutation.executeMutation(variables);
if (result.error) {
if (result.error.toString() == '[GraphQL] user/not_found') {
toast.error(t('state.user_not_found'));
} else {
toast.error(t('state.add_user_failure'));
}
} else {
toast.success(t('state.add_user_success'));
emit('member');
}
return result;
};
</script>

View File

@@ -13,7 +13,7 @@
<div class="border rounded border-divider my-8">
<HoppSmartPlaceholder
v-if="team?.teamMembers?.length === 0"
text="No members in this team. Add members to this team to collaborate"
:text="t('teams.no_members')"
>
<template #body>
<HoppButtonSecondary
@@ -35,7 +35,7 @@
>
<input
class="flex flex-1 px-4 py-3 bg-transparent"
placeholder="Email"
:placeholder="t('teams.email_title')"
:name="'param' + index"
:value="member.email"
readonly
@@ -50,7 +50,7 @@
<span class="relative">
<input
class="flex flex-1 px-4 py-3 bg-transparent cursor-pointer"
placeholder="Permissions"
:placeholder="t('teams.permissions')"
:name="'value' + index"
:value="member.role"
readonly
@@ -69,7 +69,7 @@
@keyup.escape="hide()"
>
<HoppSmartItem
label="OWNER"
:label="t('teams.owner')"
:icon="
member.role === 'OWNER' ? IconCircleDot : IconCircle
"
@@ -82,7 +82,7 @@
"
/>
<HoppSmartItem
label="EDITOR"
:label="t('teams.editor')"
:icon="
member.role === 'EDITOR' ? IconCircleDot : IconCircle
"
@@ -98,7 +98,7 @@
"
/>
<HoppSmartItem
label="VIEWER"
:label="t('teams.viewer')"
:icon="
member.role === 'VIEWER' ? IconCircleDot : IconCircle
"
@@ -131,7 +131,7 @@
</div>
</div>
</div>
<div v-if="!fetching && !team" class="flex flex-col items-center">
<div v-if="!team" class="flex flex-col items-center">
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ t('teams.error') }}
</div>
@@ -139,13 +139,15 @@
<div class="flex">
<HoppButtonPrimary
:label="t('teams.save')"
v-if="areRolesUpdated"
:label="t('teams.save_changes')"
outline
@click="saveUpdatedTeam"
/>
</div>
<TeamsInvite
:show="showInvite"
:team="team"
:editingTeamID="route.params.id.toString()"
@member="updateMembers"
@hide-modal="
@@ -158,102 +160,101 @@
</template>
<script setup lang="ts">
import IconCircleDot from '~icons/lucide/circle-dot';
import IconCircle from '~icons/lucide/circle';
import IconUserPlus from '~icons/lucide/user-plus';
import IconUserMinus from '~icons/lucide/user-minus';
import IconChevronDown from '~icons/lucide/chevron-down';
import { useClientHandle, useMutation } from '@urql/vue';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useMutation } from '@urql/vue';
import { useVModel } from '@vueuse/core';
import { cloneDeep, isEqual } from 'lodash-es';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { useClientHandler } from '~/composables/useClientHandler';
import IconChevronDown from '~icons/lucide/chevron-down';
import IconCircle from '~icons/lucide/circle';
import IconCircleDot from '~icons/lucide/circle-dot';
import IconUserMinus from '~icons/lucide/user-minus';
import IconUserPlus from '~icons/lucide/user-plus';
import {
ChangeUserRoleInTeamByAdminDocument,
TeamInfoDocument,
TeamMemberRole,
RemoveUserFromTeamByAdminDocument,
TeamInfoDocument,
TeamInfoQuery,
TeamMemberRole,
} from '../../helpers/backend/graphql';
import { HoppButtonPrimary, HoppButtonSecondary } from '@hoppscotch/ui';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
const route = useRoute();
const props = defineProps<{
team: TeamInfoQuery['infra']['teamInfo'];
}>();
const emit = defineEmits<{
(e: 'update-team'): void;
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
}>();
const teamDetails = useVModel(props, 'team', emit);
// Used to Invoke the Invite Members Modal
const showInvite = ref(false);
// Get Team Details
const team = ref<TeamInfoQuery['infra']['teamInfo'] | undefined>();
const fetching = ref(true);
const route = useRoute();
const { client } = useClientHandle();
const getTeamInfo = async () => {
fetching.value = true;
const result = await client
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
.toPromise();
if (result.error) {
return toast.error(`${t('teams.load_info_error')}`);
const { fetchData: getTeamInfo, data: teamInfo } = useClientHandler(
TeamInfoDocument,
{
teamID: route.params.id.toString(),
}
if (result.data?.infra.teamInfo) {
team.value = result.data.infra.teamInfo;
}
fetching.value = false;
};
);
onMounted(async () => await getTeamInfo());
onUnmounted(() => emit('update-team'));
onMounted(async () => {
await getTeamInfo();
});
const team = computed(() => teamInfo.value?.infra.teamInfo);
// Update members tab after a change in the members list or member roles
const updateMembers = () => {
getTeamInfo();
emit('update-team');
const updateMembers = async () => {
if (!team.value) return;
await getTeamInfo();
teamDetails.value = team.value;
};
// Template refs
const tippyActions = ref<any | null>(null);
const roleUpdates = ref<
// Roles of the members in the team
const currentMemberRoles = ref<
{
userID: string;
role: TeamMemberRole;
}[]
>([]);
watch(
() => team.value,
(teamDetails) => {
const members = teamDetails?.teamMembers ?? [];
// Roles of the members in the team after the updates but before saving
const updatedMemberRoles = ref<
{
userID: string;
role: TeamMemberRole;
}[]
>(cloneDeep(currentMemberRoles.value));
// Remove deleted members
roleUpdates.value = roleUpdates.value.filter(
(update) =>
members.findIndex(
(y: { user: { uid: string } }) => y.user.uid === update.userID
) !== -1
);
}
// Check if the roles of the members have been updated
const areRolesUpdated = computed(() =>
currentMemberRoles.value && updatedMemberRoles.value
? !isEqual(currentMemberRoles.value, updatedMemberRoles.value)
: false
);
// Update the role of the member selected in the UI
const updateMemberRole = (userID: string, role: TeamMemberRole) => {
const updateIndex = roleUpdates.value.findIndex(
const updateIndex = updatedMemberRoles.value.findIndex(
(item) => item.userID === userID
);
if (updateIndex !== -1) {
// Role Update exists
roleUpdates.value[updateIndex].role = role;
updatedMemberRoles.value[updateIndex].role = role;
} else {
// Role Update does not exist
roleUpdates.value.push({
updatedMemberRoles.value.push({
userID,
role,
});
@@ -264,7 +265,7 @@ const updateMemberRole = (userID: string, role: TeamMemberRole) => {
const membersList = computed(() => {
if (!team.value) return [];
const members = (team.value.teamMembers ?? []).map((member) => {
const updatedRole = roleUpdates.value.find(
const updatedRole = updatedMemberRoles.value.find(
(update) => update.userID === member.user.uid
);
@@ -299,19 +300,31 @@ const isLoading = ref(false);
const saveUpdatedTeam = async () => {
isLoading.value = true;
roleUpdates.value.forEach(async (update) => {
const isOwnerPresent = membersList.value.some(
(member) => member.role === TeamMemberRole.Owner
);
if (!isOwnerPresent) {
toast.error(t('state.owner_not_present'));
isLoading.value = false;
return;
}
updatedMemberRoles.value.forEach(async (update) => {
if (!team.value) return;
const updateMemberRoleResult = await changeUserRoleInTeam(
update.userID,
team.value.id,
update.role
);
if (updateMemberRoleResult.error) {
toast.error(`${t('state.role_update_failed')}`);
roleUpdates.value = [];
toast.error(t('state.role_update_failed'));
} else {
toast.success(`${t('state.role_update_success')}`);
roleUpdates.value = [];
toast.success(t('state.role_update_success'));
currentMemberRoles.value = updatedMemberRoles.value;
updatedMemberRoles.value = cloneDeep(currentMemberRoles.value);
}
isLoading.value = false;
});
@@ -340,14 +353,14 @@ const removeExistingTeamMember = async (userID: string, index: number) => {
team.value.id
)();
if (removeTeamMemberResult.error) {
toast.error(`${t('state.remove_member_failure')}`);
toast.error(t('state.remove_member_failure'));
} else {
team.value.teamMembers = team.value.teamMembers?.filter(
(member: any) => member.user.uid !== userID
);
toast.success(`${t('state.remove_member_success')}`);
teamDetails.value = team.value;
toast.success(t('state.remove_member_success'));
}
isLoadingIndex.value = null;
emit('update-team');
};
</script>

View File

@@ -1,103 +1,76 @@
<template>
<div class="border rounded divide-y divide-dividerLight border-divider my-8">
<div v-if="fetching" class="flex items-center justify-center p-4">
<HoppSmartSpinner />
</div>
<div v-else>
<div v-if="team" class="divide-y divide-dividerLight">
<div
v-for="(invitee, index) in pendingInvites"
:key="`invitee-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
v-if="invitee"
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
placeholder="Email"
:name="'param' + index"
:value="invitee.inviteeEmail"
readonly
<HoppSmartPlaceholder
v-if="team && pendingInvites?.length === 0"
text="No pending invites"
/>
<div v-else class="divide-y divide-dividerLight">
<div
v-for="(invitee, index) in pendingInvites"
:key="`invitee-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
v-if="invitee"
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
:placeholder="t('teams.email_title')"
:name="'param' + index"
:value="invitee.inviteeEmail"
readonly
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
:placeholder="t('teams.permission')"
:name="'value' + index"
:value="invitee.inviteeRole"
readonly
/>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('teams.remove')"
:icon="IconTrash"
color="red"
:loading="isLoadingIndex === index"
@click="removeInvitee(invitee.id, index)"
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
placeholder="Permissions"
:name="'value' + index"
:value="invitee.inviteeRole"
readonly
/>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('teams.remove')"
:icon="IconTrash"
color="red"
:loading="isLoadingIndex === index"
@click="removeInvitee(invitee.id, index)"
/>
</div>
</div>
</div>
<HoppSmartPlaceholder
v-if="team && pendingInvites?.length === 0"
text="No pending invites"
>
<template #body>
<div v-if="!fetching && error" class="flex flex-col items-center p-4">
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ t('error.something_went_wrong') }}
</div>
</template>
</HoppSmartPlaceholder>
</div>
</div>
</template>
<script setup lang="ts">
import IconTrash from '~icons/lucide/trash';
import { useMutation, useClientHandle } from '@urql/vue';
import { ref, onMounted } from 'vue';
import { useMutation } from '@urql/vue';
import { useVModel } from '@vueuse/core';
import { computed, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import {
RevokeTeamInvitationDocument,
TeamInfoDocument,
TeamInfoQuery,
} from '~/helpers/backend/graphql';
import { useToast } from '~/composables/toast';
import { useRoute } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import IconTrash from '~icons/lucide/trash';
const t = useI18n();
const toast = useToast();
// Get details of the team
const fetching = ref(true);
const error = ref(false);
const { client } = useClientHandle();
const route = useRoute();
const team = ref<TeamInfoQuery['infra']['teamInfo'] | undefined>();
const pendingInvites = ref<
TeamInfoQuery['infra']['teamInfo']['teamInvitations'] | undefined
>();
const props = defineProps<{
team: TeamInfoQuery['infra']['teamInfo'];
}>();
const getTeamInfo = async () => {
fetching.value = true;
const result = await client
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
.toPromise();
const emit = defineEmits<{
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
}>();
if (result.error) {
error.value = true;
return toast.error(`${t('teams.load_info_error')}`);
}
if (result.data?.infra.teamInfo) {
team.value = result.data.infra.teamInfo;
pendingInvites.value = team.value.teamInvitations;
}
fetching.value = false;
};
onMounted(async () => await getTeamInfo());
const team = useVModel(props, 'team', emit);
const pendingInvites = computed({
get: () => team.value?.teamInvitations,
set: (value) => {
team.value.teamInvitations = value;
},
});
// Remove Invitation
const isLoadingIndex = ref<null | number>(null);
@@ -110,7 +83,7 @@ const removeInvitee = async (id: string, index: number) => {
isLoadingIndex.value = index;
const result = await revokeTeamInvitation(id);
if (result.error) {
toast.error(`${t('state.remove_invitee_failure')}`);
toast.error(t('state.remove_invitee_failure'));
} else {
if (pendingInvites.value) {
pendingInvites.value = pendingInvites.value.filter(
@@ -118,7 +91,7 @@ const removeInvitee = async (id: string, index: number) => {
return invite.id !== id;
}
);
toast.success(`${t('state.remove_invitee_success')}`);
toast.success(t('state.remove_invitee_success'));
}
}
isLoadingIndex.value = null;

View File

@@ -1,6 +1,6 @@
<template>
<div class="rounded-md">
<div class="grid gap-6 mt-4">
<div class="grid gap-6">
<div
class="relative"
:class="
@@ -71,18 +71,14 @@
<script setup lang="ts">
import { format } from 'date-fns';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { UserInfoQuery } from '~/helpers/backend/graphql';
import IconTrash from '~icons/lucide/trash';
import IconUserCheck from '~icons/lucide/user-check';
import IconUserMinus from '~icons/lucide/user-minus';
import { UserInfoQuery } from '~/helpers/backend/graphql';
const t = useI18n();
const toast = useToast();
const props = defineProps<{

View File

@@ -3,7 +3,7 @@
v-if="show"
dialog
:title="t('users.invite_user')"
@close="$emit('hide-modal')"
@close="emit('hide-modal')"
>
<template #body>
<HoppSmartInput
@@ -16,7 +16,6 @@
<span class="flex space-x-2">
<HoppButtonPrimary
:label="t('users.send_invite')"
:loading="loadingState"
@click="sendInvite"
/>
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
@@ -27,21 +26,18 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useToast } from '~/composables/toast';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
const t = useI18n();
const toast = useToast();
withDefaults(
defineProps<{
show: boolean;
loadingState: boolean;
}>(),
{
show: false,
loadingState: false,
}
);
@@ -54,7 +50,7 @@ const email = ref('');
const sendInvite = () => {
if (email.value.trim() === '') {
toast.error(`${t('users.valid_email')}`);
toast.error(t('users.valid_email'));
return;
}
emit('send-invite', email.value);

View File

@@ -1,116 +1,116 @@
<template>
<div>
<div class="px-4">
<div v-if="fetching" class="flex justify-center">
<HoppSmartSpinner />
</div>
<div v-else-if="error">{{ t('shared_requests.load_list_error') }}</div>
<div v-else-if="sharedRequests.length === 0" class="ml-3 mt-5 text-lg">
<div v-else-if="sharedRequests.length === 0" class="mt-5">
{{ t('users.no_shared_requests') }}
</div>
<div v-else class="mt-10">
<HoppSmartTable :list="sharedRequests">
<template #head>
<tr
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
>
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
<!-- Empty Heading for the Action Button -->
<th class="px-6 py-2 text-center">Actions</th>
</tr>
</template>
<template #body="{ list: sharedRequests }">
<tr
v-for="request in sharedRequests"
:key="request.id"
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
>
<td class="flex py-4 px-7 max-w-50">
<span class="truncate">
{{ request.id }}
</span>
</td>
<HoppSmartTable v-else class="mt-8" :list="sharedRequests">
<template #head>
<tr
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
>
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
<!-- Empty Heading for the Action Button -->
<th class="px-6 py-2 text-center">
{{ t('shared_requests.action') }}
</th>
</tr>
</template>
<template #body="{ list: sharedRequests }">
<tr
v-for="request in sharedRequests"
:key="request.id"
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
>
<td class="flex py-4 px-7 max-w-50">
<span class="truncate">
{{ request.id }}
</span>
</td>
<td class="py-4 px-7 w-96">
{{ sharedRequestURL(request.request) }}
</td>
<td class="py-4 px-7 w-96">
{{ sharedRequestURL(request.request) }}
</td>
<td class="py-2 px-7">
{{ getCreatedDate(request.createdOn) }}
<div class="text-gray-400 text-tiny">
{{ getCreatedTime(request.createdOn) }}
</div>
</td>
<td class="py-2 px-7">
{{ getCreatedDate(request.createdOn) }}
<div class="text-gray-400 text-tiny">
{{ getCreatedTime(request.createdOn) }}
</div>
</td>
<td class="flex justify-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.open_request')"
:to="`${shortcodeBaseURL}/r/${request.id}`"
:blank="true"
:icon="IconExternalLink"
class="px-3 text-emerald-500 hover:text-accent"
/>
<td class="flex justify-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.open_request')"
:to="`${shortcodeBaseURL}/r/${request.id}`"
:blank="true"
:icon="IconExternalLink"
class="px-3 text-emerald-500 hover:text-accent"
/>
<UiAutoResetIcon
:title="t('shared_requests.copy')"
:icon="{ default: IconCopy, temporary: IconCheck }"
@click="copySharedRequest(request.id)"
/>
<UiAutoResetIcon
:title="t('shared_requests.copy')"
:icon="{ default: IconCopy, temporary: IconCheck }"
@click="copySharedRequest(request.id)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.delete')"
:icon="IconTrash"
color="red"
class="px-3"
@click="deleteSharedRequest(request.id)"
/>
</td>
</tr>
</template>
</HoppSmartTable>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('shared_requests.delete')"
:icon="IconTrash"
color="red"
class="px-3"
@click="deleteSharedRequest(request.id)"
/>
</td>
</tr>
</template>
</HoppSmartTable>
<!-- Pagination -->
<div
v-if="hasNextPage && sharedRequests.length >= sharedRequestsPerPage"
class="flex items-center w-28 px-3 py-2 mt-5 mx-auto font-semibold text-secondaryDark bg-divider hover:bg-dividerDark rounded-3xl cursor-pointer"
@click="fetchNextSharedRequests"
>
<span class="mr-2">{{ t('shared_requests.show_more') }}</span>
<icon-lucide-chevron-down />
</div>
<!-- Pagination -->
<div
v-if="hasNextPage && sharedRequests.length >= sharedRequestsPerPage"
class="flex items-center w-28 px-3 py-2 mt-5 mx-auto font-semibold text-secondaryDark bg-divider hover:bg-dividerDark rounded-3xl cursor-pointer"
@click="fetchNextSharedRequests"
>
<span class="mr-2">{{ t('shared_requests.show_more') }}</span>
<icon-lucide-chevron-down />
</div>
</div>
<HoppSmartConfirmModal
:show="confirmDeletion"
:title="t('shared_requests.confirm_request_deletion')"
@hide-modal="confirmDeletion = false"
@resolve="deleteSharedRequestMutation(deleteSharedRequestID)"
/>
<HoppSmartConfirmModal
:show="confirmDeletion"
:title="t('shared_requests.confirm_request_deletion')"
@hide-modal="confirmDeletion = false"
@resolve="deleteSharedRequestMutation(deleteSharedRequestID)"
/>
</div>
</template>
<script setup lang="ts">
import { useMutation } from '@urql/vue';
import { format } from 'date-fns';
import { ref } from 'vue';
import { useMutation } from '@urql/vue';
import {
SharedRequestsDocument,
RevokeShortcodeByAdminDocument,
} from '../../helpers/backend/graphql';
import { usePagedQuery } from '~/composables/usePagedQuery';
import { useToast } from '~/composables/toast';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { usePagedQuery } from '~/composables/usePagedQuery';
import { copyToClipboard } from '~/helpers/utils/clipboard';
import IconTrash from '~icons/lucide/trash';
import IconCopy from '~icons/lucide/copy';
import IconCheck from '~icons/lucide/check';
import IconCopy from '~icons/lucide/copy';
import IconExternalLink from '~icons/lucide/external-link';
import IconTrash from '~icons/lucide/trash';
import {
RevokeShortcodeByAdminDocument,
SharedRequestsDocument,
} from '~/helpers/backend/graphql';
const t = useI18n();
const toast = useToast();
@@ -154,7 +154,7 @@ const shortcodeBaseURL =
// Copy Shared Request to Clipboard
const copySharedRequest = (requestID: string) => {
copyToClipboard(`${shortcodeBaseURL}/r/${requestID}`);
toast.success(`${t('state.copied_to_clipboard')}`);
toast.success(t('state.copied_to_clipboard'));
};
// Shared Request Deletion
@@ -170,19 +170,19 @@ const deleteSharedRequest = (id: string) => {
const deleteSharedRequestMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error(`${t('state.delete_request_failure')}`);
toast.error(t('state.delete_request_failure'));
return;
}
const variables = { codeID: id };
await sharedRequestDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.delete_request_failure')}`);
toast.error(t('state.delete_request_failure'));
} else {
sharedRequests.value = sharedRequests.value.filter(
(request) => request.id !== id
);
refetch();
toast.success(`${t('state.delete_request_success')}`);
toast.success(t('state.delete_request_success'));
}
});
confirmDeletion.value = false;

View File

@@ -1,6 +1,6 @@
import { TypedDocumentNode, useClientHandle } from '@urql/vue';
import { DocumentNode } from 'graphql';
import { ref } from 'vue';
import { Ref, ref } from 'vue';
/** A composable function to handle grapqhl requests
* using urql's useClientHandle
@@ -14,15 +14,16 @@ export function useClientHandler<
ListItem
>(
query: string | TypedDocumentNode<Result, Vars> | DocumentNode,
getList: (result: Result) => ListItem[],
variables: Vars
variables: Vars,
getList?: (result: Result) => ListItem[]
) {
const { client } = useClientHandle();
const fetching = ref(true);
const error = ref(false);
const list = ref<ListItem[]>([]);
const data = ref<Result>();
const dataAsList: Ref<ListItem[]> = ref([]);
const fetchList = async () => {
const fetchData = async () => {
fetching.value = true;
try {
const result = await client
@@ -31,9 +32,12 @@ export function useClientHandler<
})
.toPromise();
const resultList = getList(result.data!);
list.value.push(...resultList);
if (getList) {
const resultList = getList(result.data!);
dataAsList.value.push(...resultList);
} else {
data.value = result.data;
}
} catch (e) {
error.value = true;
}
@@ -43,7 +47,8 @@ export function useClientHandler<
return {
fetching,
error,
list,
fetchList,
data,
dataAsList,
fetchData,
};
}

View File

@@ -72,31 +72,35 @@ export function useConfigHandler(updatedConfigs?: Config) {
const {
fetching: fetchingInfraConfigs,
error: infraConfigsError,
list: infraConfigs,
fetchList: fetchInfraConfigs,
} = useClientHandler(InfraConfigsDocument, (x) => x.infraConfigs, {
configNames: [
'GOOGLE_CLIENT_ID',
'GOOGLE_CLIENT_SECRET',
'MICROSOFT_CLIENT_ID',
'MICROSOFT_CLIENT_SECRET',
'GITHUB_CLIENT_ID',
'GITHUB_CLIENT_SECRET',
'MAILER_SMTP_URL',
'MAILER_ADDRESS_FROM',
] as InfraConfigEnum[],
});
dataAsList: infraConfigs,
fetchData: fetchInfraConfigs,
} = useClientHandler(
InfraConfigsDocument,
{
configNames: [
'GOOGLE_CLIENT_ID',
'GOOGLE_CLIENT_SECRET',
'MICROSOFT_CLIENT_ID',
'MICROSOFT_CLIENT_SECRET',
'GITHUB_CLIENT_ID',
'GITHUB_CLIENT_SECRET',
'MAILER_SMTP_URL',
'MAILER_ADDRESS_FROM',
] as InfraConfigEnum[],
},
(x) => x.infraConfigs
);
// Fetching allowed auth providers
const {
fetching: fetchingAllowedAuthProviders,
error: allowedAuthProvidersError,
list: allowedAuthProviders,
fetchList: fetchAllowedAuthProviders,
dataAsList: allowedAuthProviders,
fetchData: fetchAllowedAuthProviders,
} = useClientHandler(
AllowedAuthProvidersDocument,
(x) => x.allowedAuthProviders,
{}
{},
(x) => x.allowedAuthProviders
);
// Current and working configs
@@ -255,7 +259,21 @@ export function useConfigHandler(updatedConfigs?: Config) {
return config;
});
// Trasforming the working configs back into the format required by the mutations
// Checking if any of the config fields are empty
const isFieldEmpty = (field: string) => field.trim() === '';
const AreAnyConfigFieldsEmpty = (config: Config): boolean => {
const providerFieldsEmpty = Object.values(config.providers).some(
(provider) => Object.values(provider.fields).some(isFieldEmpty)
);
const mailFieldsEmpty = Object.values(config.mailConfigs.fields).some(
isFieldEmpty
);
return providerFieldsEmpty || mailFieldsEmpty;
};
// Transforming the working configs back into the format required by the mutations
const updatedAllowedAuthProviders = computed(() => {
return [
{
@@ -341,5 +359,6 @@ export function useConfigHandler(updatedConfigs?: Config) {
fetchingAllowedAuthProviders,
infraConfigsError,
allowedAuthProvidersError,
AreAnyConfigFieldsEmpty,
};
}

View File

@@ -16,15 +16,15 @@
{{ t('configs.load_error') }}
</div>
<div v-else class="flex flex-col py-8">
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
<HoppSmartTab :id="'config'" :label="t('configs.title')">
<SettingsConfigurations
v-model:config="workingConfigs"
class="py-8 px-4"
/>
</HoppSmartTab>
</HoppSmartTabs>
<div v-else-if="workingConfigs" class="flex flex-col py-8">
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
<HoppSmartTab :id="'config'" :label="t('configs.title')">
<SettingsConfigurations
v-model:config="workingConfigs"
class="py-8 px-4"
/>
</HoppSmartTab>
</HoppSmartTabs>
</div>
<div v-if="isConfigUpdated" class="fixed bottom-0 right-0 m-10">
@@ -43,7 +43,7 @@
:show="showSaveChangesModal"
:title="t('configs.confirm_changes')"
@hide-modal="showSaveChangesModal = false"
@resolve="initiateServerRestart = true"
@resolve="restartServer"
/>
</template>
@@ -51,9 +51,11 @@
import { isEqual } from 'lodash-es';
import { computed, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { useConfigHandler } from '~/composables/useConfigHandler';
const t = useI18n();
const toast = useToast();
const showSaveChangesModal = ref(false);
const initiateServerRestart = ref(false);
@@ -70,6 +72,7 @@ const {
infraConfigsError,
fetchingAllowedAuthProviders,
allowedAuthProvidersError,
AreAnyConfigFieldsEmpty,
} = useConfigHandler();
// Check if the configs have been updated
@@ -78,4 +81,17 @@ const isConfigUpdated = computed(() =>
? !isEqual(currentConfigs.value, workingConfigs.value)
: false
);
// Check if any of the fields in workingConfigs are empty
const areAnyFieldsEmpty = computed(() =>
workingConfigs.value ? AreAnyConfigFieldsEmpty(workingConfigs.value) : false
);
const restartServer = () => {
if (areAnyFieldsEmpty.value) {
return toast.error(t('configs.input_empty'));
}
initiateServerRestart.value = true;
showSaveChangesModal.value = false;
};
</script>

View File

@@ -4,7 +4,9 @@
<HoppSmartSpinner />
</div>
<div v-if="team" class="flex flex-col">
<div v-else-if="error">{{ t('teams.load_info_error') }}</div>
<div v-else-if="team" class="flex flex-col">
<div class="flex items-center space-x-4">
<button
class="p-2 rounded-3xl bg-divider hover:bg-dividerDark transition flex justify-center items-center"
@@ -27,19 +29,16 @@
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
<HoppSmartTab :id="'details'" :label="t('teams.details')">
<TeamsDetails
:team="team"
:teamName="teamName"
v-model:showRenameInput="showRenameInput"
@rename-team="renameTeamName"
v-model:team="team"
@delete-team="deleteTeam"
class="py-8 px-4"
/>
</HoppSmartTab>
<HoppSmartTab :id="'members'" :label="t('teams.team_members')">
<TeamsMembers @update-team="updateTeam()" class="py-8 px-4" />
<TeamsMembers v-model:team="team" class="py-8 px-4" />
</HoppSmartTab>
<HoppSmartTab :id="'invites'" :label="t('teams.invites')">
<TeamsPendingInvites :editingTeamID="team.id" />
<TeamsPendingInvites v-model:team="team" />
</HoppSmartTab>
</HoppSmartTabs>
@@ -55,24 +54,24 @@
</template>
<script setup lang="ts">
import { useClientHandle, useMutation } from '@urql/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useMutation } from '@urql/vue';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { useClientHandler } from '~/composables/useClientHandler';
import {
RemoveTeamDocument,
RenameTeamDocument,
TeamInfoDocument,
TeamMemberRole,
TeamInfoQuery,
} from '../../helpers/backend/graphql';
import { HoppSmartTabs } from '@hoppscotch/ui';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
const route = useRoute();
const router = useRouter();
// Tabs
type OptionTabs = 'details' | 'members' | 'invites';
const selectedOptionTab = ref<OptionTabs>('details');
@@ -90,59 +89,24 @@ const currentTabName = computed(() => {
}
});
// Get the details of the team
// Get Team Info
const {
fetching,
error,
data: teamInfo,
fetchData: getTeamInfo,
} = useClientHandler(TeamInfoDocument, {
teamID: route.params.id.toString(),
});
const team = ref<TeamInfoQuery['infra']['teamInfo'] | undefined>();
const teamName = ref('');
const route = useRoute();
const fetching = ref(true);
const { client } = useClientHandle();
const getTeamInfo = async () => {
fetching.value = true;
const result = await client
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
.toPromise();
if (result.error) {
return toast.error(`${t('team.load_info_error')}`);
}
if (result.data?.infra.teamInfo) {
team.value = result.data.infra.teamInfo;
teamName.value = team.value.name;
}
fetching.value = false;
};
onMounted(async () => await getTeamInfo());
const updateTeam = async () => await getTeamInfo();
// Rename the team name
const showRenameInput = ref(false);
const teamRename = useMutation(RenameTeamDocument);
const renameTeamName = async (teamName: string) => {
if (!team.value) return;
if (team.value.name === teamName) {
showRenameInput.value = false;
return;
}
const variables = { uid: team.value.id, name: teamName };
await teamRename.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.rename_team_failure')}`);
} else {
showRenameInput.value = false;
if (team.value) {
team.value.name = teamName;
toast.success(`${t('state.rename_team_success')}`);
}
}
});
};
onMounted(async () => {
await getTeamInfo();
team.value = teamInfo.value?.infra.teamInfo;
});
// Delete team from the infra
const router = useRouter();
const confirmDeletion = ref(false);
const teamDeletion = useMutation(RemoveTeamDocument);
const deleteTeamUID = ref<string | null>(null);
@@ -155,42 +119,18 @@ const deleteTeam = (id: string) => {
const deleteTeamMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error(`${t('state.delete_team_failure')}`);
toast.error(t('state.delete_team_failure'));
return;
}
const variables = { uid: id };
await teamDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.delete_team_failure')}`);
} else {
toast.success(`${t('state.delete_team_success')}`);
}
});
const result = await teamDeletion.executeMutation(variables);
if (result.error) {
toast.error(t('state.delete_team_failure'));
} else {
toast.success(t('state.delete_team_success'));
}
confirmDeletion.value = false;
deleteTeamUID.value = null;
router.push('/teams');
};
// Update Roles of Members
const roleUpdates = ref<
{
userID: string;
role: TeamMemberRole;
}[]
>([]);
watch(
() => team.value,
(teamDetails) => {
const members = teamDetails?.teamMembers ?? [];
// Remove deleted members
roleUpdates.value = roleUpdates.value.filter(
(update) =>
members.findIndex(
(y: { user: { uid: string } }) => y.user.uid === update.userID
) !== -1
);
}
);
</script>

View File

@@ -122,7 +122,15 @@
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { useMutation, useQuery } from '@urql/vue';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { usePagedQuery } from '~/composables/usePagedQuery';
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
import IconAddUsers from '~icons/lucide/plus';
import IconTrash from '~icons/lucide/trash';
import {
CreateTeamDocument,
MetricsDocument,
@@ -130,18 +138,10 @@ import {
TeamListDocument,
UsersListDocument,
} from '../../helpers/backend/graphql';
import { usePagedQuery } from '~/composables/usePagedQuery';
import { computed, ref, watch } from 'vue';
import { useMutation, useQuery } from '@urql/vue';
import { useToast } from '~/composables/toast';
import IconAddUsers from '~icons/lucide/plus';
import IconTrash from '~icons/lucide/trash';
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
// Get Users List
const { data } = useQuery({ query: MetricsDocument });
const usersPerPage = computed(() => data.value?.infra.usersCount || 10000);
@@ -174,55 +174,50 @@ const {
);
// Create Team
const createTeamMutation = useMutation(CreateTeamDocument);
const showCreateTeamModal = ref(false);
const createTeamLoading = ref(false);
const createTeamMutation = useMutation(CreateTeamDocument);
const createTeam = async (newTeamName: string, ownerEmail: string) => {
if (newTeamName.length < 6) {
toast.error(`${t('state.team_name_long')}`);
toast.error(t('state.team_name_too_short'));
return;
}
if (ownerEmail.length === 0) {
toast.error(`${t('state.enter_team_email')}`);
toast.error(t('state.enter_team_email'));
return;
}
createTeamLoading.value = true;
const userUid =
usersList.value.find((user) => user.email === ownerEmail)?.uid || '';
const variables = { name: newTeamName.trim(), userUid: userUid };
await createTeamMutation.executeMutation(variables).then((result) => {
if (result.error) {
if (result.error.toString() == '[GraphQL] user/not_found') {
toast.error(`${t('state.user_not_found')}`);
} else {
toast.error(`${t('state.create_team_failure')}`);
}
createTeamLoading.value = false;
const result = await createTeamMutation.executeMutation(variables);
if (result.error) {
if (result.error.toString() == '[GraphQL] user/not_found') {
toast.error(t('state.user_not_found'));
} else {
toast.success(`${t('state.create_team_success')}`);
showCreateTeamModal.value = false;
createTeamLoading.value = false;
refetch();
toast.error(t('state.create_team_failure'));
}
});
createTeamLoading.value = false;
} else {
toast.success(t('state.create_team_success'));
showCreateTeamModal.value = false;
createTeamLoading.value = false;
refetch();
}
};
// Go To Individual Team Details Page
const router = useRouter();
const goToTeamDetails = (teamId: string) => router.push('/teams/' + teamId);
// Reload Teams Page when routed back to the teams page
const route = useRoute();
watch(
() => route.params.id,
() => window.location.reload()
);
// Team Deletion
const teamDeletion = useMutation(RemoveTeamDocument);
const confirmDeletion = ref(false);
const deleteTeamID = ref<string | null>(null);
const teamDeletion = useMutation(RemoveTeamDocument);
const deleteTeam = (id: string) => {
confirmDeletion.value = true;
@@ -232,20 +227,19 @@ const deleteTeam = (id: string) => {
const deleteTeamMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error(`${t('state.delete_team_failure')}`);
toast.error(t('state.delete_team_failure'));
return;
}
const variables = { uid: id };
await teamDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.delete_team_failure')}`);
} else {
teamsList.value = teamsList.value.filter((team) => team.id !== id);
toast.success(`${t('state.delete_team_success')}`);
}
});
const result = await teamDeletion.executeMutation(variables);
if (result.error) {
toast.error(t('state.delete_team_failure'));
} else {
teamsList.value = teamsList.value.filter((team) => team.id !== id);
toast.success(t('state.delete_team_success'));
}
confirmDeletion.value = false;
deleteTeamID.value = null;
router.push('/teams');
};
</script>

View File

@@ -1,6 +1,7 @@
<template>
<div v-if="fetching" class="flex justify-center"><HoppSmartSpinner /></div>
<div v-else class="flex flex-col space-y-4">
<div v-else-if="error">{{ t('users.load_info_error') }}</div>
<div v-else-if="user" class="flex flex-col space-y-4">
<div class="flex gap-x-3">
<button
class="p-2 mb-2 rounded-3xl bg-divider"
@@ -32,7 +33,7 @@
/>
</HoppSmartTab>
<HoppSmartTab :id="'requests'" :label="t('shared_requests.title')">
<UsersSharedRequests :email="user.email" class="py-8 px-4 mt-10" />
<UsersSharedRequests :email="user.email" />
</HoppSmartTab>
</HoppSmartTabs>
</div>
@@ -59,21 +60,20 @@
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue';
import { useMutation } from '@urql/vue';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { useClientHandler } from '~/composables/useClientHandler';
import {
MakeUserAdminDocument,
UserInfoDocument,
RemoveUserByAdminDocument,
RemoveUserAsAdminDocument,
RemoveUserByAdminDocument,
UserInfoDocument,
} from '~/helpers/backend/graphql';
import { useClientHandle } from '@urql/vue';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from '~/composables/toast';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
// Tabs
@@ -91,23 +91,26 @@ const currentTabName = computed(() => {
}
});
// Get User Info
const user = ref();
const { client } = useClientHandle();
const fetching = ref(true);
const route = useRoute();
onMounted(async () => {
fetching.value = true;
const result = await client
.query(UserInfoDocument, { uid: route.params.id.toString() })
.toPromise();
if (result.error) {
toast.error(`${t('users.load_info_error')}`);
const { fetching, error, data, fetchData } = useClientHandler(
UserInfoDocument,
{
uid: route.params.id.toString(),
}
user.value = result.data?.infra.userInfo ?? {};
fetching.value = false;
);
onMounted(async () => {
await fetchData();
});
const user = computed({
get: () => data.value?.infra.userInfo,
set: (value) => {
if (value) {
data.value!.infra.userInfo = value;
}
},
});
// User Deletion
@@ -124,17 +127,18 @@ const deleteUser = (id: string) => {
const deleteUserMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error(`${t('state.delete_user_failure')}`);
toast.error(t('state.delete_user_failure'));
return;
}
const variables = { uid: id };
await userDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.delete_user_failure')}`);
} else {
toast.success(`${t('state.delete_user_success')}`);
}
});
const result = await userDeletion.executeMutation(variables);
if (result.error) {
toast.error(t('state.delete_user_failure'));
} else {
toast.success(t('state.delete_user_success'));
}
confirmDeletion.value = false;
deleteUserUID.value = null;
router.push('/users');
@@ -153,18 +157,17 @@ const makeUserAdmin = (id: string) => {
const makeUserAdminMutation = async (id: string | null) => {
if (!id) {
confirmUserToAdmin.value = false;
toast.error(`${t('state.admin_failure')}`);
toast.error(t('state.admin_failure'));
return;
}
const variables = { uid: id };
await userToAdmin.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.admin_failure')}`);
} else {
user.value.isAdmin = true;
toast.success(`${t('state.admin_success')}`);
}
});
const result = await userToAdmin.executeMutation(variables);
if (result.error) {
toast.error(t('state.admin_failure'));
} else {
user.value!.isAdmin = true;
toast.success(t('state.admin_success'));
}
confirmUserToAdmin.value = false;
userToAdminUID.value = null;
};
@@ -182,18 +185,17 @@ const makeAdminToUser = (id: string) => {
const makeAdminToUserMutation = async (id: string | null) => {
if (!id) {
confirmAdminToUser.value = false;
toast.error(`${t('state.remove_admin_failure')}`);
toast.error(t('state.remove_admin_failure'));
return;
}
const variables = { uid: id };
await adminToUser.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.remove_admin_failure')}`);
} else {
user.value.isAdmin = false;
toast.error(`${t('state.remove_admin_success')}`);
}
});
const result = await adminToUser.executeMutation(variables);
if (result.error) {
toast.error(t('state.remove_admin_failure'));
} else {
user.value!.isAdmin = false;
toast.error(t('state.remove_admin_success'));
}
confirmAdminToUser.value = false;
adminToUserUID.value = null;
};

View File

@@ -11,7 +11,6 @@
@click="showInviteUserModal = true"
:icon="IconAddUser"
/>
<div class="flex">
<HoppButtonSecondary
outline
@@ -173,35 +172,33 @@
</template>
<script setup lang="ts">
import { format } from 'date-fns';
import { ref, watch } from 'vue';
import { useMutation } from '@urql/vue';
import { format } from 'date-fns';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { usePagedQuery } from '~/composables/usePagedQuery';
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
import IconTrash from '~icons/lucide/trash';
import IconUserCheck from '~icons/lucide/user-check';
import IconUserMinus from '~icons/lucide/user-minus';
import IconAddUser from '~icons/lucide/user-plus';
import {
InviteNewUserDocument,
MakeUserAdminDocument,
RemoveUserByAdminDocument,
RemoveUserAsAdminDocument,
RemoveUserByAdminDocument,
UsersListDocument,
} from '../../helpers/backend/graphql';
import { usePagedQuery } from '~/composables/usePagedQuery';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from '~/composables/toast';
import { HoppButtonSecondary } from '@hoppscotch/ui';
import IconAddUser from '~icons/lucide/user-plus';
import IconTrash from '~icons/lucide/trash';
import IconUserMinus from '~icons/lucide/user-minus';
import IconUserCheck from '~icons/lucide/user-check';
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
import { useI18n } from '~/composables/i18n';
} from '~/helpers/backend/graphql';
// Get Proper Date Formats
const t = useI18n();
const toast = useToast();
const getCreatedDate = (date: string) => format(new Date(date), 'dd-MM-yyyy');
const getCreatedTime = (date: string) => format(new Date(date), 'hh:mm a');
const t = useI18n();
const toast = useToast();
// Get Paginated Results of all the users in the infra
const usersPerPage = 20;
const {
@@ -224,30 +221,23 @@ const showInviteUserModal = ref(false);
const sendInvite = async (email: string) => {
if (!email.trim()) {
toast.error(`${t('state.invalid_email')}`);
toast.error(t('state.invalid_email'));
return;
}
const variables = { inviteeEmail: email.trim() };
await sendInvitation.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.email_failure')}`);
} else {
toast.success(`${t('state.email_success')}`);
showInviteUserModal.value = false;
}
});
const result = await sendInvitation.executeMutation(variables);
if (result.error) {
toast.error(t('state.email_failure'));
} else {
toast.success(t('state.email_success'));
showInviteUserModal.value = false;
}
};
// Go to Individual User Details Page
const route = useRoute();
const router = useRouter();
const goToUserDetails = (uid: string) => router.push('/users/' + uid);
watch(
() => route.params.id,
() => window.location.reload()
);
// User Deletion
const userDeletion = useMutation(RemoveUserByAdminDocument);
const confirmDeletion = ref(false);
@@ -256,18 +246,17 @@ const deleteUserUID = ref<string | null>(null);
const deleteUserMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error(`${t('state.delete_user_failure')}`);
toast.error(t('state.delete_user_failure'));
return;
}
const variables = { uid: id };
await userDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.delete_user_failure')}`);
} else {
toast.success(`${t('state.delete_user_success')}`);
usersList.value = usersList.value.filter((user) => user.uid !== id);
}
});
const result = await userDeletion.executeMutation(variables);
if (result.error) {
toast.error(t('state.delete_user_failure'));
} else {
toast.success(t('state.delete_user_success'));
usersList.value = usersList.value.filter((user) => user.uid !== id);
}
confirmDeletion.value = false;
deleteUserUID.value = null;
};
@@ -285,23 +274,20 @@ const makeUserAdmin = (id: string) => {
const makeUserAdminMutation = async (id: string | null) => {
if (!id) {
confirmUserToAdmin.value = false;
toast.error(`${t('state.admin_failure')}`);
toast.error(t('state.admin_failure'));
return;
}
const variables = { uid: id };
await userToAdmin.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.admin_failure')}`);
} else {
toast.success(`${t('state.admin_success')}`);
usersList.value = usersList.value.map((user) => {
if (user.uid === id) {
user.isAdmin = true;
}
return user;
});
}
});
const result = await userToAdmin.executeMutation(variables);
if (result.error) {
toast.error(t('state.admin_failure'));
} else {
toast.success(t('state.admin_success'));
usersList.value = usersList.value.map((user) => ({
...user,
isAdmin: user.uid === id ? true : user.isAdmin,
}));
}
confirmUserToAdmin.value = false;
userToAdminUID.value = null;
};
@@ -324,23 +310,20 @@ const deleteUser = (id: string) => {
const makeAdminToUserMutation = async (id: string | null) => {
if (!id) {
confirmAdminToUser.value = false;
toast.error(`${t('state.remove_admin_failure')}`);
toast.error(t('state.remove_admin_failure'));
return;
}
const variables = { uid: id };
await adminToUser.executeMutation(variables).then((result) => {
if (result.error) {
toast.error(`${t('state.remove_admin_failure')}`);
} else {
toast.success(`${t('state.remove_admin_success')}`);
usersList.value = usersList.value.map((user) => {
if (user.uid === id) {
user.isAdmin = false;
}
return user;
});
}
});
const result = await adminToUser.executeMutation(variables);
if (result.error) {
toast.error(t('state.remove_admin_failure'));
} else {
toast.success(t('state.remove_admin_success'));
usersList.value = usersList.value.map((user) => ({
...user,
isAdmin: user.uid === id ? false : user.isAdmin,
}));
}
confirmAdminToUser.value = false;
adminToUserUID.value = null;
};

View File

@@ -46,16 +46,14 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useQuery } from '@urql/vue';
import { InvitedUsersDocument } from '../../helpers/backend/graphql';
import { format } from 'date-fns';
import { HoppSmartSpinner } from '@hoppscotch/ui';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
import { InvitedUsersDocument } from '~/helpers/backend/graphql';
const t = useI18n();
const router = useRouter();
// Get Proper Date Formats