Compare commits

..

7 Commits

Author SHA1 Message Date
nivedin
51ebc198de fix: close context menu when typing 2024-01-23 22:01:31 +05:30
nivedin
7406a241e6 refactor: disable context menu for test and pre request test 2024-01-23 22:01:31 +05:30
nivedin
f51196604a refactor: add option to disable context menu 2024-01-23 22:01:31 +05:30
Anwarul Islam
c0dbcc901f fix: documentation is not being generated on GQL (#3730)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2024-01-23 22:00:41 +05:30
Joel Jacob Stephen
ba52c8cc37 refactor: improvements to the dashboard sidebar (#3709)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 21:55:42 +05:30
Dmitry
d1f6f40ef8 fix: perform logout if the silent refresh attempt fails (#3705)
Co-authored-by: Dmitry Mukovkin <d.mukovkin@cft.ru>
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 21:53:59 +05:30
Akash K
99f5070f71 fix: unwanted double slashes when importing from openapi (#3745) 2024-01-23 21:49:34 +05:30
16 changed files with 118 additions and 318 deletions

View File

@@ -269,25 +269,6 @@ export class AdminResolver {
return invitedUser.right;
}
@Mutation(() => Boolean, { description: 'Revoke a user invite by ID' })
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async revokeUserInvitationByAdmin(
@GqlAdmin() adminUser: Admin,
@Args({
name: 'inviteeEmail',
description: 'Invite Email',
type: () => ID,
})
inviteeEmail: string,
): Promise<boolean> {
const invite = await this.adminService.revokeUserInvite(
inviteeEmail,
adminUser.uid,
);
if (E.isLeft(invite)) throwErr(invite.left);
return invite.right;
}
@Mutation(() => Boolean, {
description: 'Delete an user account from infra',
})
@@ -490,14 +471,4 @@ export class AdminResolver {
userInvited(@GqlUser() admin: AuthUser) {
return this.pubsub.asyncIterator(`admin/${admin.uid}/invited`);
}
@Subscription(() => InvitedUser, {
description: 'Listen for User Revocation',
resolve: (value) => value,
})
@SkipThrottle()
@UseGuards(GqlAuthGuard, GqlAdminGuard)
userRevoked(@GqlUser() admin: AuthUser) {
return this.pubsub.asyncIterator(`admin/${admin.uid}/revoked`);
}
}

View File

@@ -1,7 +1,7 @@
import { AdminService } from './admin.service';
import { PubSubService } from '../pubsub/pubsub.service';
import { mockDeep } from 'jest-mock-extended';
import { InvitedUsers as DbInvitedUser } from '@prisma/client';
import { InvitedUsers } from '@prisma/client';
import { UserService } from '../user/user.service';
import { TeamService } from '../team/team.service';
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
@@ -14,11 +14,9 @@ import {
DUPLICATE_EMAIL,
INVALID_EMAIL,
USER_ALREADY_INVITED,
USER_NOT_INVITED,
} from '../errors';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
import { InvitedUser } from './invited-user.model';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -46,7 +44,7 @@ const adminService = new AdminService(
mockConfigService,
);
const invitedUsers: DbInvitedUser[] = [
const invitedUsers: InvitedUsers[] = [
{
adminUid: 'uid1',
adminEmail: 'admin1@example.com',
@@ -66,19 +64,9 @@ describe('AdminService', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
mockPrisma.user.findMany.mockResolvedValue([]);
const expectedResults: InvitedUser[] = invitedUsers.map(
(invitedUser) => ({
...invitedUser,
isInvitationAccepted: false,
}),
);
const results = await adminService.fetchInvitedUsers();
expect(results).toEqual(expectedResults);
expect(results).toEqual(invitedUsers);
});
test('should resolve left and return an empty array if invited users not found', async () => {
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
@@ -88,51 +76,6 @@ describe('AdminService', () => {
});
});
describe('revokeUserInvite', () => {
test('should resolve left and return error if email not invited', async () => {
mockPrisma.invitedUsers.delete.mockRejectedValueOnce(new Error());
const result = await adminService.revokeUserInvite(
'test@gmail.com',
'adminUid',
);
expect(result).toEqualLeft(USER_NOT_INVITED);
});
test('should resolve right and return deleted invitee email', async () => {
const adminUid = 'adminUid';
mockPrisma.invitedUsers.delete.mockResolvedValueOnce(invitedUsers[0]);
const result = await adminService.revokeUserInvite(
invitedUsers[0].inviteeEmail,
adminUid,
);
expect(mockPrisma.invitedUsers.delete).toHaveBeenCalledWith({
where: {
inviteeEmail: invitedUsers[0].inviteeEmail,
},
});
expect(result).toEqualRight(true);
});
test('should resolve right, delete invitee email and publish a subscription', async () => {
const adminUid = 'adminUid';
mockPrisma.invitedUsers.delete.mockResolvedValueOnce(invitedUsers[0]);
await adminService.revokeUserInvite(
invitedUsers[0].inviteeEmail,
adminUid,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`admin/${adminUid}/revoked`,
invitedUsers[0],
);
});
});
describe('inviteUserToSignInViaEmail', () => {
test('should resolve right and create a invited user', async () => {
mockPrisma.invitedUsers.findFirst.mockResolvedValueOnce(null);

View File

@@ -11,10 +11,10 @@ import {
INVALID_EMAIL,
ONLY_ONE_ADMIN_ACCOUNT,
TEAM_INVITE_ALREADY_MEMBER,
TEAM_INVITE_NO_INVITE_FOUND,
USER_ALREADY_INVITED,
USER_IS_ADMIN,
USER_NOT_FOUND,
USER_NOT_INVITED,
} from '../errors';
import { MailerService } from '../mailer/mailer.service';
import { InvitedUser } from './invited-user.model';
@@ -26,7 +26,6 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
import { TeamMemberRole } from '../team/team.model';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
import { Admin } from './admin.model';
@Injectable()
export class AdminService {
@@ -111,69 +110,18 @@ export class AdminService {
return E.right(invitedUser);
}
/**
* Revoke the invitation of a user to join infra.
* @param inviteeEmail Invitee's email
* @param adminUser Admin object
* @returns an Either of array of `InvitedUser` object or error string
*/
async revokeUserInvite(inviteeEmail: string, adminUid: string) {
try {
const deletedInvitee = await this.prisma.invitedUsers.delete({
where: {
inviteeEmail,
},
});
const invitedUser = <InvitedUser>{
adminEmail: deletedInvitee.adminEmail,
adminUid: deletedInvitee.adminUid,
inviteeEmail: deletedInvitee.inviteeEmail,
invitedOn: deletedInvitee.invitedOn,
};
this.pubsub.publish(`admin/${adminUid}/revoked`, invitedUser);
return E.right(true);
} catch (error) {
return E.left(USER_NOT_INVITED);
}
}
/**
* Fetch the list of invited users by the admin.
* @returns an Either of array of `InvitedUser` object or error
*/
async fetchInvitedUsers() {
const dbInvitedUsers = await this.prisma.invitedUsers.findMany();
const invitedUsers = await this.prisma.invitedUsers.findMany();
const invitationAcceptedUsers = await this.prisma.user.findMany({
where: {
email: {
in: dbInvitedUsers.map((user) => user.inviteeEmail),
},
},
});
const users: InvitedUser[] = invitedUsers.map(
(user) => <InvitedUser>{ ...user },
);
let invitedUsers: InvitedUser[] = [];
dbInvitedUsers.forEach((dbInvitedUser) => {
const isUserAccepts = invitationAcceptedUsers.find(
(user) => user.email === dbInvitedUser.inviteeEmail,
);
const invitedUser: InvitedUser = {
adminEmail: dbInvitedUser.adminEmail,
adminUid: dbInvitedUser.adminUid,
inviteeEmail: dbInvitedUser.inviteeEmail,
invitedOn: dbInvitedUser.invitedOn,
isInvitationAccepted: isUserAccepts ? true : false,
};
invitedUsers.push(invitedUser);
});
return invitedUsers;
return users;
}
/**

View File

@@ -21,10 +21,4 @@ export class InvitedUser {
description: 'Date when the user invitation was sent',
})
invitedOn: Date;
@Field({
description: 'Boolean status if invitation was accepted or not',
defaultValue: false,
})
isInvitationAccepted: boolean;
}

View File

@@ -65,11 +65,6 @@ export const USER_FB_DOCUMENT_DELETION_FAILED =
*/
export const USER_NOT_FOUND = 'user/not_found' as const;
/**
* User is not invited by admin
*/
export const USER_NOT_INVITED = 'admin/user_not_invited' as const;
/**
* User is already invited by admin
*/

View File

@@ -31,7 +31,7 @@ import { Shortcode } from 'src/shortcode/shortcode.model';
// A custom message type that defines the topic and the corresponding payload.
// For every module that publishes a subscription add its type def and the possible subscription type.
export type TopicDef = {
[topic: `admin/${string}/${'invited' | 'revoked'}`]: InvitedUser;
[topic: `admin/${string}/${'invited'}`]: InvitedUser;
[topic: `user/${string}/${'updated' | 'deleted'}`]: User;
[topic: `user_settings/${string}/${'created' | 'updated'}`]: UserSettings;
[

View File

@@ -187,7 +187,7 @@ import IconClock from "~icons/lucide/clock"
import IconCopy from "~icons/lucide/copy"
import IconBox from "~icons/lucide/box"
import { computed, nextTick, reactive, ref } from "vue"
import { GraphQLField, GraphQLType } from "graphql"
import { GraphQLField, GraphQLType, getNamedType } from "graphql"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror"
import { copyToClipboard } from "@helpers/utils/clipboard"
@@ -260,12 +260,6 @@ function getFilteredGraphqlTypes(filterText: string, types: GraphQLType[]) {
})
}
function resolveRootType(type: GraphQLType) {
let t: any = type
while (t.ofType) t = t.ofType
return t
}
const toast = useToast()
const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>(
@@ -331,7 +325,7 @@ const handleJumpToType = async (type: GraphQLType) => {
selectedGqlTab.value = "types"
await nextTick()
const rootTypeName = resolveRootType(type).name
const rootTypeName = getNamedType(type).name
const target = document.getElementById(`type_${rootTypeName}`)
if (target) {
target.scrollIntoView({ block: "center", behavior: "smooth" })

View File

@@ -8,7 +8,7 @@
</template>
<script setup lang="ts">
import { GraphQLScalarType, GraphQLType } from "graphql"
import { GraphQLScalarType, GraphQLType, getNamedType } from "graphql"
import { computed } from "vue"
const props = defineProps<{
@@ -21,15 +21,9 @@ const emit = defineEmits<{
const typeString = computed(() => `${props.gqlType}`)
const isScalar = computed(() => {
return resolveRootType(props.gqlType) instanceof GraphQLScalarType
return getNamedType(props.gqlType) instanceof GraphQLScalarType
})
function resolveRootType(type: GraphQLType) {
let t = type as any
while (t.ofType !== null) t = t.ofType
return t
}
function jumpToType() {
if (isScalar.value) return
emit("jump-to-type", props.gqlType)

View File

@@ -99,6 +99,7 @@ useCodemirror(
linter,
completer,
environmentHighlights: false,
contextMenuEnabled: false,
})
)

View File

@@ -95,6 +95,7 @@ useCodemirror(
linter,
completer,
environmentHighlights: false,
contextMenuEnabled: false,
})
)

View File

@@ -79,6 +79,7 @@ const props = withDefaults(
readonly?: boolean
autoCompleteSource?: string[]
inspectionResults?: InspectorResult[] | undefined
contextMenuEnabled?: boolean
}>(),
{
modelValue: "",
@@ -91,6 +92,7 @@ const props = withDefaults(
autoCompleteSource: undefined,
inspectionResult: undefined,
inspectionResults: undefined,
contextMenuEnabled: true,
}
)
@@ -359,8 +361,11 @@ const initView = (el: any) => {
handleTextSelection()
}, 140)
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
// Only add event listeners if context menu is enabled in the component
if (props.contextMenuEnabled) {
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
}
const extensions: Extension = [
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
@@ -396,7 +401,7 @@ const initView = (el: any) => {
ev.preventDefault()
},
scroll(event) {
if (event.target) {
if (event.target && props.contextMenuEnabled) {
handleTextSelection()
}
},
@@ -405,7 +410,6 @@ const initView = (el: any) => {
class {
update(update: ViewUpdate) {
if (props.readonly) return
if (update.docChanged) {
const prevValue = clone(cachedValue.value)
@@ -436,6 +440,17 @@ const initView = (el: any) => {
clipboardEv = null
pastedValue = null
}
if (props.contextMenuEnabled) {
// close the context menu if text is being updated in the editor
invokeAction("contextmenu.open", {
position: {
top: 0,
left: 0,
},
text: null,
})
}
}
}
}

View File

@@ -63,6 +63,8 @@ type CodeMirrorOptions = {
additionalExts?: Extension[]
contextMenuEnabled?: boolean
// callback on editor update
onUpdate?: (view: ViewUpdate) => void
}
@@ -208,6 +210,9 @@ export function useCodemirror(
): { cursor: Ref<{ line: number; ch: number }> } {
const { subscribeToStream } = useStreamSubscriber()
// Set default value for contextMenuEnabled if not provided
options.contextMenuEnabled = options.contextMenuEnabled ?? true
const additionalExts = new Compartment()
const language = new Compartment()
const lineWrapping = new Compartment()
@@ -272,8 +277,11 @@ export function useCodemirror(
handleTextSelection()
}, 140)
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
// Only add event listeners if context menu is enabled in the editor
if (options.contextMenuEnabled) {
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
}
if (options.onUpdate) {
options.onUpdate(update)
@@ -312,7 +320,7 @@ export function useCodemirror(
),
EditorView.domEventHandlers({
scroll(event) {
if (event.target) {
if (event.target && options.contextMenuEnabled) {
handleTextSelection()
}
},

View File

@@ -549,13 +549,19 @@ const convertPathToHoppReqs = (
),
// Construct request object
RA.map(({ method, info }) =>
makeRESTRequest({
RA.map(({ method, info }) => {
const openAPIUrl = parseOpenAPIUrl(doc)
const openAPIPath = replaceOpenApiPathTemplating(pathName)
const endpoint =
openAPIUrl.endsWith("/") && openAPIPath.startsWith("/")
? openAPIUrl + openAPIPath.slice(1)
: openAPIUrl + openAPIPath
return makeRESTRequest({
name: info.operationId ?? info.summary ?? "Untitled Request",
method: method.toUpperCase(),
endpoint: `${parseOpenAPIUrl(doc)}${replaceOpenApiPathTemplating(
pathName
)}`,
endpoint,
// We don't need to worry about reference types as the Dereferencing pass should remove them
params: parseOpenAPIParams(
@@ -572,7 +578,7 @@ const convertPathToHoppReqs = (
preRequestScript: "",
testScript: "",
})
),
}),
// Disable Readonly
RA.toArray

View File

@@ -114,6 +114,7 @@ async function setInitialUser() {
} else {
setUser(null)
isGettingInitialUser.value = false
await logout()
}
return
@@ -146,22 +147,26 @@ async function setInitialUser() {
}
async function refreshToken() {
const res = await axios.get(
`${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`,
{
withCredentials: true,
try {
const res = await axios.get(
`${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`,
{
withCredentials: true,
}
)
const isSuccessful = res.status === 200
if (isSuccessful) {
authEvents$.next({
event: "token_refresh",
})
}
)
const isSuccessful = res.status === 200
if (isSuccessful) {
authEvents$.next({
event: "token_refresh",
})
return isSuccessful
} catch (error) {
return false
}
return isSuccessful
}
async function sendMagicLink(email: string) {

View File

@@ -1,9 +1,9 @@
// 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 {
@@ -13,8 +13,6 @@ declare module '@vue/runtime-core' {
AppModal: typeof import('./components/app/Modal.vue')['default']
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
AppToast: typeof import('./components/app/Toast.vue')['default']
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
@@ -23,6 +21,7 @@ declare module '@vue/runtime-core' {
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']
@@ -30,42 +29,13 @@ declare module '@vue/runtime-core' {
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']
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']
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
SmartCheckbox: typeof import('./../../hoppscotch-ui/src/components/smart/Checkbox.vue')['default']
SmartConfirmModal: typeof import('./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue')['default']
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
SmartTable: typeof import('./../../hoppscotch-ui/src/components/smart/Table.vue')['default']
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
SmartTreeBranch: typeof import('./../../hoppscotch-ui/src/components/smart/TreeBranch.vue')['default']
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.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']
@@ -76,27 +46,6 @@ declare module '@vue/runtime-core' {
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'];
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
IconLucideInbox: typeof import('~icons/lucide/inbox')['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'];
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
}
}

View File

@@ -12,7 +12,7 @@
:class="isOpen ? '' : '-translate-x-full ease-in'"
class="sidebar-container transform !md:translate-x-0 ease-out"
>
<div :class="isExpanded ? 'w-80' : 'w-full'">
<div :class="isExpanded ? 'w-56' : 'w-full'">
<div class="flex items-center justify-start px-4 my-4">
<div class="flex items-center">
<HoppSmartLink class="flex items-center space-x-4" to="/dashboard">
@@ -26,7 +26,6 @@
</HoppSmartLink>
</div>
</div>
<nav class="my-5">
<HoppSmartLink
v-for="(navigation, index) in primaryNavigations"
@@ -39,19 +38,32 @@
:to="navigation.to"
tabindex="0"
:exact="navigation.exact"
class="nav-link"
:class="
!isExpanded
? 'flex items-center justify-center'
: 'flex items-center'
"
@click="setActiveTab(navigation.label)"
>
<div v-if="navigation.icon">
<component :is="navigation.icon" class="svg-icons" />
<div
class="flex p-5 w-full font-bold"
:class="
activeTab === navigation.label
? 'bg-primaryDark text-secondaryDark border-l-2 border-l-emerald-600'
: 'bg-primary hover:bg-primaryLight hover:text-secondaryDark focus-visible:text-secondaryDark focus-visible:bg-primaryLight focus-visible:outline-none'
"
>
<div
v-if="navigation.icon"
class="svg-icons"
:class="isExpanded ? 'mr-3' : 'mx-auto'"
>
<component :is="navigation.icon" />
</div>
<span v-if="isExpanded" class="nav-title">
{{ navigation.label }}
</span>
</div>
<span v-if="isExpanded" class="nav-title">
{{ navigation.label }}
</span>
</HoppSmartLink>
</nav>
</div>
@@ -60,19 +72,27 @@
</template>
<script setup lang="ts">
import { HoppSmartLink } from '@hoppscotch/ui';
import { ref, type Component } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useSidebar } from '~/composables/useSidebar';
import IconDashboard from '~icons/lucide/layout-dashboard';
import IconSettings from '~icons/lucide/settings';
import IconUser from '~icons/lucide/user';
import IconUsers from '~icons/lucide/users';
import IconSettings from '~icons/lucide/settings';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const { isOpen, isExpanded } = useSidebar();
const primaryNavigations = [
type NavigationItem = {
label: string;
icon: Component;
to: string;
exact: boolean;
};
const primaryNavigations: NavigationItem[] = [
{
label: t('metrics.dashboard'),
icon: IconDashboard,
@@ -98,6 +118,12 @@ const primaryNavigations = [
exact: true,
},
];
const activeTab = ref('Dashboard');
const setActiveTab = (tab: string) => {
activeTab.value = tab;
};
</script>
<style scoped lang="scss">
@@ -106,54 +132,4 @@ const primaryNavigations = [
@apply transition duration-300;
@apply flex overflow-y-auto bg-primary border-r border-divider;
}
.nav-link {
@apply relative;
@apply p-4;
@apply flex flex-1;
@apply items-center;
@apply space-x-4;
@apply hover:bg-primaryDark hover:text-secondaryDark;
@apply focus-visible:text-secondaryDark;
@apply after:absolute;
@apply after:inset-x-0;
@apply after:md:inset-x-auto;
@apply after:md:inset-y-0;
@apply after:bottom-0;
@apply after:md:bottom-auto;
@apply after:md:left-0;
@apply after:z-10;
@apply after:h-0.5;
@apply after:md:h-full;
@apply after:w-full;
@apply after:md:w-0.5;
@apply after:content-[''];
@apply focus:after:bg-divider;
.svg-icons {
@apply opacity-75;
}
&.router-link-active {
@apply text-secondaryDark;
@apply bg-primaryLight;
@apply hover:text-secondaryDark;
@apply after:bg-accent;
.svg-icons {
@apply opacity-100;
}
}
&.exact-active-link {
@apply text-secondaryDark;
@apply bg-primaryLight;
@apply hover:text-secondaryDark;
@apply after:bg-accent;
.svg-icons {
@apply opacity-100;
}
}
}
</style>