Compare commits
24 Commits
fix/genera
...
feat-cherr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eb067feaf | ||
|
|
0b5f57436f | ||
|
|
0c0ed5610e | ||
|
|
dce032a275 | ||
|
|
2599b1d326 | ||
|
|
419e376f46 | ||
|
|
c79fcbeceb | ||
|
|
092cb4c3a5 | ||
|
|
d3f25361f7 | ||
|
|
09c13e86b2 | ||
|
|
04bb219c12 | ||
|
|
ca79cf40b1 | ||
|
|
454c82975e | ||
|
|
c38488dfc4 | ||
|
|
2d0ebedbbb | ||
|
|
88f6a4ae26 | ||
|
|
610538ca02 | ||
|
|
8970ff5c68 | ||
|
|
d1a564d5b8 | ||
|
|
8bb1d19c07 | ||
|
|
5a516f7242 | ||
|
|
3b217d78e7 | ||
|
|
8e153b38dc | ||
|
|
6f38bfb148 |
@@ -13,6 +13,7 @@ SESSION_SECRET='add some secret here'
|
|||||||
# Hoppscotch App Domain Config
|
# Hoppscotch App Domain Config
|
||||||
REDIRECT_URL="http://localhost:3000"
|
REDIRECT_URL="http://localhost:3000"
|
||||||
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||||
|
ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||||
|
|
||||||
# Google Auth Config
|
# Google Auth Config
|
||||||
GOOGLE_CLIENT_ID="************************************************"
|
GOOGLE_CLIENT_ID="************************************************"
|
||||||
|
|||||||
@@ -411,6 +411,23 @@ export class AdminResolver {
|
|||||||
return deletedTeam.right;
|
return deletedTeam.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
description: 'Revoke a team Invite by Invite ID',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async revokeTeamInviteByAdmin(
|
||||||
|
@Args({
|
||||||
|
name: 'inviteID',
|
||||||
|
description: 'Team Invite ID',
|
||||||
|
type: () => ID,
|
||||||
|
})
|
||||||
|
inviteID: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const invite = await this.adminService.revokeTeamInviteByID(inviteID);
|
||||||
|
if (E.isLeft(invite)) throwErr(invite.left);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|
||||||
@Subscription(() => InvitedUser, {
|
@Subscription(() => InvitedUser, {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
ONLY_ONE_ADMIN_ACCOUNT,
|
ONLY_ONE_ADMIN_ACCOUNT,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
USER_IS_ADMIN,
|
USER_IS_ADMIN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
@@ -416,4 +417,19 @@ export class AdminService {
|
|||||||
if (E.isLeft(team)) return E.left(team.left);
|
if (E.isLeft(team)) return E.left(team.left);
|
||||||
return E.right(team.right);
|
return E.right(team.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a team invite by ID
|
||||||
|
* @param inviteID Team Invite ID
|
||||||
|
* @returns an Either of boolean or error
|
||||||
|
*/
|
||||||
|
async revokeTeamInviteByID(inviteID: string) {
|
||||||
|
const teamInvite = await this.teamInvitationService.revokeInvitation(
|
||||||
|
inviteID,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(teamInvite)) return E.left(teamInvite.left);
|
||||||
|
|
||||||
|
return E.right(teamInvite.right);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
InternalServerErrorException,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Req,
|
|
||||||
Request,
|
Request,
|
||||||
Res,
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -19,12 +19,18 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||||
import { authCookieHandler, throwHTTPErr } from './helper';
|
import {
|
||||||
|
AuthProvider,
|
||||||
|
authCookieHandler,
|
||||||
|
authProviderCheck,
|
||||||
|
throwHTTPErr,
|
||||||
|
} from './helper';
|
||||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
@@ -39,6 +45,9 @@ export class AuthController {
|
|||||||
@Body() authData: SignInMagicDto,
|
@Body() authData: SignInMagicDto,
|
||||||
@Query('origin') origin: string,
|
@Query('origin') origin: string,
|
||||||
) {
|
) {
|
||||||
|
if (!authProviderCheck(AuthProvider.EMAIL))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
const deviceIdToken = await this.authService.signInMagicLink(
|
const deviceIdToken = await this.authService.signInMagicLink(
|
||||||
authData.email,
|
authData.email,
|
||||||
origin,
|
origin,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
|
|||||||
import { GoogleStrategy } from './strategies/google.strategy';
|
import { GoogleStrategy } from './strategies/google.strategy';
|
||||||
import { GithubStrategy } from './strategies/github.strategy';
|
import { GithubStrategy } from './strategies/github.strategy';
|
||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -26,9 +27,9 @@ import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
|||||||
AuthService,
|
AuthService,
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
RTJwtStrategy,
|
RTJwtStrategy,
|
||||||
GoogleStrategy,
|
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
|
||||||
GithubStrategy,
|
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
|
||||||
MicrosoftStrategy,
|
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.GITHUB))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.GOOGLE))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard extends AuthGuard('microsoft') {
|
export class MicrosoftSSOGuard
|
||||||
|
extends AuthGuard('microsoft')
|
||||||
|
implements CanActivate
|
||||||
|
{
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
||||||
|
throwHTTPErr({
|
||||||
|
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ForbiddenException, HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { AuthError } from 'src/types/AuthError';
|
||||||
import { AuthTokens } from 'src/types/AuthTokens';
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { COOKIES_NOT_FOUND } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
||||||
|
import { throwErr } from 'src/utils';
|
||||||
|
|
||||||
enum AuthTokenType {
|
enum AuthTokenType {
|
||||||
ACCESS_TOKEN = 'access_token',
|
ACCESS_TOKEN = 'access_token',
|
||||||
@@ -16,6 +17,13 @@ export enum Origin {
|
|||||||
APP = 'app',
|
APP = 'app',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AuthProvider {
|
||||||
|
GOOGLE = 'GOOGLE',
|
||||||
|
GITHUB = 'GITHUB',
|
||||||
|
MICROSOFT = 'MICROSOFT',
|
||||||
|
EMAIL = 'EMAIL',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function allows throw to be used as an expression
|
* This function allows throw to be used as an expression
|
||||||
* @param errMessage Message present in the error message
|
* @param errMessage Message present in the error message
|
||||||
@@ -97,3 +105,25 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
|||||||
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
|
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if given auth provider is present in the ALLOWED_AUTH_PROVIDERS env variable
|
||||||
|
*
|
||||||
|
* @param provider Provider we want to check the presence of
|
||||||
|
* @returns Boolean if provider specified is present or not
|
||||||
|
*/
|
||||||
|
export function authProviderCheck(provider: string) {
|
||||||
|
if (!provider) {
|
||||||
|
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envVariables = process.env.ALLOWED_AUTH_PROVIDERS
|
||||||
|
? process.env.ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||||
|
provider.trim().toUpperCase(),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!envVariables.includes(provider.toUpperCase())) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,30 @@ export const AUTH_FAIL = 'auth/fail';
|
|||||||
*/
|
*/
|
||||||
export const JSON_INVALID = 'json_invalid';
|
export const JSON_INVALID = 'json_invalid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth Provider not specified
|
||||||
|
* (Auth)
|
||||||
|
*/
|
||||||
|
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
||||||
|
*/
|
||||||
|
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
|
||||||
|
'"ALLOWED_AUTH_PROVIDERS" is not present in .env file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
||||||
|
*/
|
||||||
|
export const ENV_EMPTY_AUTH_PROVIDERS =
|
||||||
|
'"ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
|
||||||
|
*/
|
||||||
|
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
|
||||||
|
'"ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to delete a user data document from fb firestore but failed.
|
* Tried to delete a user data document from fb firestore but failed.
|
||||||
* (FirebaseService)
|
* (FirebaseService)
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import * as cookieParser from 'cookie-parser';
|
|||||||
import { VersioningType } from '@nestjs/common';
|
import { VersioningType } from '@nestjs/common';
|
||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import { emitGQLSchemaFile } from './gql-schema';
|
import { emitGQLSchemaFile } from './gql-schema';
|
||||||
|
import { checkEnvironmentAuthProvider } from './utils';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||||
console.log(`Port: ${process.env.PORT}`);
|
console.log(`Port: ${process.env.PORT}`);
|
||||||
|
|
||||||
|
checkEnvironmentAuthProvider();
|
||||||
|
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import * as E from 'fp-ts/Either';
|
|||||||
import * as A from 'fp-ts/Array';
|
import * as A from 'fp-ts/Array';
|
||||||
import { TeamMemberRole } from './team/team.model';
|
import { TeamMemberRole } from './team/team.model';
|
||||||
import { User } from './user/user.model';
|
import { User } from './user/user.model';
|
||||||
import { JSON_INVALID } from './errors';
|
import { ENV_EMPTY_AUTH_PROVIDERS, ENV_NOT_FOUND_KEY_AUTH_PROVIDERS, ENV_NOT_SUPPORT_AUTH_PROVIDERS, JSON_INVALID } from './errors';
|
||||||
|
import { AuthProvider } from './auth/helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workaround to throw an exception in an expression.
|
* A workaround to throw an exception in an expression.
|
||||||
@@ -152,3 +153,31 @@ export function isValidLength(title: string, length: number) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called by bootstrap() in main.ts
|
||||||
|
* It checks if the "ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||||
|
* If not, it throws an error.
|
||||||
|
*/
|
||||||
|
export function checkEnvironmentAuthProvider() {
|
||||||
|
if (!process.env.hasOwnProperty('ALLOWED_AUTH_PROVIDERS')) {
|
||||||
|
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.ALLOWED_AUTH_PROVIDERS === '') {
|
||||||
|
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const givenAuthProviders = process.env.ALLOWED_AUTH_PROVIDERS.split(',').map(
|
||||||
|
(provider) => provider.toLocaleUpperCase(),
|
||||||
|
);
|
||||||
|
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||||
|
(provider: string) => provider.toLocaleUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const givenAuthProvider of givenAuthProviders) {
|
||||||
|
if (!supportedAuthProviders.includes(givenAuthProvider)) {
|
||||||
|
throw new Error(ENV_NOT_SUPPORT_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@apply after:backface-hidden;
|
@apply after:backface-hidden;
|
||||||
@apply selection:bg-accentDark;
|
@apply selection:bg-accentDark;
|
||||||
@apply selection:text-accentContrast;
|
@apply selection:text-accentContrast;
|
||||||
|
@apply overscroll-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -151,6 +151,11 @@
|
|||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||||
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
||||||
},
|
},
|
||||||
|
"context_menu": {
|
||||||
|
"set_environment_variable": "Set as variable",
|
||||||
|
"add_parameter": "Add to parameter",
|
||||||
|
"open_link_in_new_tab": "Open link in new tab"
|
||||||
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"header": "Header {count}",
|
"header": "Header {count}",
|
||||||
"message": "Message {count}",
|
"message": "Message {count}",
|
||||||
@@ -195,16 +200,23 @@
|
|||||||
"created": "Environment created",
|
"created": "Environment created",
|
||||||
"deleted": "Environment deletion",
|
"deleted": "Environment deletion",
|
||||||
"edit": "Edit Environment",
|
"edit": "Edit Environment",
|
||||||
|
"global": "Global",
|
||||||
"invalid_name": "Please provide a name for the environment",
|
"invalid_name": "Please provide a name for the environment",
|
||||||
"my_environments": "My Environments",
|
"my_environments": "My Environments",
|
||||||
|
"name": "Name",
|
||||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||||
"new": "New Environment",
|
"new": "New Environment",
|
||||||
"no_environment": "No environment",
|
"no_environment": "No environment",
|
||||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||||
|
"replace_with_variable": "Replace with variable",
|
||||||
|
"scope": "Scope",
|
||||||
"select": "Select environment",
|
"select": "Select environment",
|
||||||
|
"set_as_environment": "Set as environment",
|
||||||
"team_environments": "Team Environments",
|
"team_environments": "Team Environments",
|
||||||
"title": "Environments",
|
"title": "Environments",
|
||||||
"updated": "Environment updated",
|
"updated": "Environment updated",
|
||||||
|
"value": "Value",
|
||||||
|
"variable": "Variable",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
|||||||
@@ -89,7 +89,6 @@
|
|||||||
"util": "^0.12.4",
|
"util": "^0.12.4",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"vue": "^3.2.25",
|
"vue": "^3.2.25",
|
||||||
"vue-github-button": "^3.0.3",
|
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-pdf-embed": "^1.1.4",
|
"vue-pdf-embed": "^1.1.4",
|
||||||
"vue-router": "^4.0.16",
|
"vue-router": "^4.0.16",
|
||||||
@@ -111,7 +110,7 @@
|
|||||||
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
|
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
|
||||||
"@graphql-codegen/urql-introspection": "^2.2.0",
|
"@graphql-codegen/urql-introspection": "^2.2.0",
|
||||||
"@graphql-typed-document-node/core": "^3.1.1",
|
"@graphql-typed-document-node/core": "^3.1.1",
|
||||||
"@iconify-json/lucide": "^1.1.40",
|
"@iconify-json/lucide": "^1.1.109",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "^2.1.1",
|
||||||
"@rushstack/eslint-patch": "^1.1.4",
|
"@rushstack/eslint-patch": "^1.1.4",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ declare module '@vue/runtime-core' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||||
|
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
|
||||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||||
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
||||||
@@ -54,6 +55,7 @@ declare module '@vue/runtime-core' {
|
|||||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||||
Environments: typeof import('./components/environments/index.vue')['default']
|
Environments: typeof import('./components/environments/index.vue')['default']
|
||||||
|
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
||||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||||
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
||||||
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
||||||
@@ -132,6 +134,7 @@ declare module '@vue/runtime-core' {
|
|||||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
|
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="contextMenuRef"
|
||||||
|
class="fixed bg-popover shadow-lg transform translate-y-8 border border-dividerDark p-2 rounded"
|
||||||
|
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`"
|
||||||
|
>
|
||||||
|
<div v-if="contextMenuOptions" class="flex flex-col">
|
||||||
|
<div
|
||||||
|
v-for="option in contextMenuOptions"
|
||||||
|
:key="option.id"
|
||||||
|
class="flex flex-col space-y-2"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="option.text.type === 'text' && option.text"
|
||||||
|
:icon="option.icon"
|
||||||
|
:label="option.text.text"
|
||||||
|
@click="handleClick(option)"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
:is="option.text.component"
|
||||||
|
v-else-if="option.text.type === 'custom'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onClickOutside } from "@vueuse/core"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { ContextMenuResult, ContextMenuService } from "~/services/context-menu"
|
||||||
|
import { EnvironmentMenuService } from "~/services/context-menu/menu/environment.menu"
|
||||||
|
import { ParameterMenuService } from "~/services/context-menu/menu/parameter.menu"
|
||||||
|
import { URLMenuService } from "~/services/context-menu/menu/url.menu"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
position: { top: number; left: number }
|
||||||
|
text: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const contextMenuRef = ref<any | null>(null)
|
||||||
|
|
||||||
|
const contextMenuOptions = ref<ContextMenuResult[]>([])
|
||||||
|
|
||||||
|
onClickOutside(contextMenuRef, () => {
|
||||||
|
emit("hide-modal")
|
||||||
|
})
|
||||||
|
|
||||||
|
const contextMenuService = useService(ContextMenuService)
|
||||||
|
|
||||||
|
useService(EnvironmentMenuService)
|
||||||
|
useService(ParameterMenuService)
|
||||||
|
useService(URLMenuService)
|
||||||
|
|
||||||
|
const handleClick = (option: { action: () => void }) => {
|
||||||
|
option.action()
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.show, props.text],
|
||||||
|
(val) => {
|
||||||
|
if (val && props.text) {
|
||||||
|
const options = contextMenuService.getMenuFor(props.text)
|
||||||
|
contextMenuOptions.value = options
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -15,18 +15,21 @@
|
|||||||
:label="t('app.name')"
|
:label="t('app.name')"
|
||||||
to="/"
|
to="/"
|
||||||
/>
|
/>
|
||||||
<!-- <AppGitHubStarButton class="mt-1.5 transition" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center space-x-2">
|
<div class="inline-flex items-center justify-center flex-1 space-x-2">
|
||||||
<HoppButtonSecondary
|
<button
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
class="flex flex-1 items-center justify-between px-2 py-1 bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-xs hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
||||||
:title="`${t(
|
|
||||||
'app.search'
|
|
||||||
)} <kbd>${getPlatformSpecialKey()}</kbd> <kbd>K</kbd>`"
|
|
||||||
:icon="IconSearch"
|
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle')"
|
||||||
/>
|
>
|
||||||
|
<span class="inline-flex flex-1 items-center">
|
||||||
|
<icon-lucide-search class="mr-2 svg-icons" />
|
||||||
|
{{ t("app.search") }}
|
||||||
|
</span>
|
||||||
|
<span class="flex">
|
||||||
|
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
|
||||||
|
<kbd class="shortcut-key">K</kbd>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="showInstallButton"
|
v-if="showInstallButton"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -44,6 +47,8 @@
|
|||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
@click="invokeAction('modals.support.toggle')"
|
@click="invokeAction('modals.support.toggle')"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="inline-flex items-center justify-end flex-1 space-x-2">
|
||||||
<div
|
<div
|
||||||
v-if="currentUser === null"
|
v-if="currentUser === null"
|
||||||
class="inline-flex items-center space-x-2"
|
class="inline-flex items-center space-x-2"
|
||||||
@@ -238,7 +243,6 @@ import IconDownload from "~icons/lucide/download"
|
|||||||
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
import IconUserPlus from "~icons/lucide/user-plus"
|
||||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||||
import IconSearch from "~icons/lucide/search"
|
|
||||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
|||||||
@@ -15,7 +15,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col divide-y divide-dividerLight">
|
<div class="flex flex-col divide-y divide-dividerLight">
|
||||||
<HoppSmartPlaceholder v-if="isEmpty(shortcutsResults)">
|
<HoppSmartPlaceholder
|
||||||
|
v-if="isEmpty(shortcutsResults)"
|
||||||
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
|
>
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<details
|
<details
|
||||||
|
|||||||
@@ -22,10 +22,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
<kbd class="shortcut-key">K</kbd>
|
<kbd class="shortcut-key">/</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">/</kbd>
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
|
<kbd class="shortcut-key">K</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">?</kbd>
|
<kbd class="shortcut-key">?</kbd>
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
|||||||
import IconEdit from "~icons/lucide/edit"
|
import IconEdit from "~icons/lucide/edit"
|
||||||
import IconFolder from "~icons/lucide/folder"
|
import IconFolder from "~icons/lucide/folder"
|
||||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||||
import { PropType, ref, computed, watch } from "vue"
|
import { ref, computed, watch } from "vue"
|
||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
@@ -209,67 +209,36 @@ type FolderType = "collection" | "folder"
|
|||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = withDefaults(
|
||||||
id: {
|
defineProps<{
|
||||||
type: String,
|
id: string
|
||||||
default: "",
|
parentID?: string | null
|
||||||
required: true,
|
data: HoppCollection<HoppRESTRequest> | TeamCollection
|
||||||
},
|
|
||||||
parentID: {
|
|
||||||
type: String as PropType<string | null>,
|
|
||||||
default: null,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,
|
|
||||||
default: () => ({}),
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
collectionsType: {
|
|
||||||
type: String as PropType<CollectionType>,
|
|
||||||
default: "my-collections",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Collection component can be used for both collections and folders.
|
* Collection component can be used for both collections and folders.
|
||||||
* folderType is used to determine which one it is.
|
* folderType is used to determine which one it is.
|
||||||
*/
|
*/
|
||||||
folderType: {
|
collectionsType: CollectionType
|
||||||
type: String as PropType<FolderType>,
|
folderType: FolderType
|
||||||
default: "collection",
|
isOpen: boolean
|
||||||
required: true,
|
isSelected?: boolean | null
|
||||||
},
|
exportLoading?: boolean
|
||||||
isOpen: {
|
hasNoTeamAccess?: boolean
|
||||||
type: Boolean,
|
collectionMoveLoading?: string[]
|
||||||
default: false,
|
isLastItem?: boolean
|
||||||
required: true,
|
}>(),
|
||||||
},
|
{
|
||||||
isSelected: {
|
id: "",
|
||||||
type: Boolean as PropType<boolean | null>,
|
parentID: null,
|
||||||
default: false,
|
collectionsType: "my-collections",
|
||||||
required: false,
|
folderType: "collection",
|
||||||
},
|
isOpen: false,
|
||||||
exportLoading: {
|
isSelected: false,
|
||||||
type: Boolean,
|
exportLoading: false,
|
||||||
default: false,
|
hasNoTeamAccess: false,
|
||||||
required: false,
|
isLastItem: false,
|
||||||
},
|
}
|
||||||
hasNoTeamAccess: {
|
)
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
collectionMoveLoading: {
|
|
||||||
type: Array as PropType<string[]>,
|
|
||||||
default: () => [],
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
isLastItem: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "toggle-children"): void
|
(event: "toggle-children"): void
|
||||||
@@ -448,8 +417,13 @@ const notSameDestination = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isCollLoading = computed(() => {
|
const isCollLoading = computed(() => {
|
||||||
if (props.collectionMoveLoading.length > 0 && props.data.id) {
|
const { collectionMoveLoading } = props
|
||||||
return props.collectionMoveLoading.includes(props.data.id)
|
if (
|
||||||
|
collectionMoveLoading &&
|
||||||
|
collectionMoveLoading.length > 0 &&
|
||||||
|
props.data.id
|
||||||
|
) {
|
||||||
|
return collectionMoveLoading.includes(props.data.id)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
208
packages/hoppscotch-common/src/components/environments/Add.vue
Normal file
208
packages/hoppscotch-common/src/components/environments/Add.vue
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<HoppSmartModal
|
||||||
|
v-if="show"
|
||||||
|
:title="t('environment.set_as_environment')"
|
||||||
|
@close="hideModal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="flex space-y-4 flex-1 flex-col">
|
||||||
|
<div class="flex items-center space-x-8 ml-2">
|
||||||
|
<label for="name" class="font-semibold min-w-10">{{
|
||||||
|
t("environment.name")
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
v-model="name"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('environment.variable')"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-8 ml-2">
|
||||||
|
<label for="value" class="font-semibold min-w-10">{{
|
||||||
|
t("environment.value")
|
||||||
|
}}</label>
|
||||||
|
<input type="text" :value="value" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-8 ml-2">
|
||||||
|
<label for="scope" class="font-semibold min-w-10">
|
||||||
|
{{ t("environment.scope") }}
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="relative flex flex-1 flex-col border border-divider rounded focus-visible:border-dividerDark"
|
||||||
|
>
|
||||||
|
<EnvironmentsSelector v-model="scope" :is-scope-selector="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="replaceWithVariable" class="flex space-x-2 mt-3">
|
||||||
|
<div class="min-w-18" />
|
||||||
|
<HoppSmartCheckbox
|
||||||
|
:on="replaceWithVariable"
|
||||||
|
title="t('environment.replace_with_variable'))"
|
||||||
|
@change="replaceWithVariable = !replaceWithVariable"
|
||||||
|
/>
|
||||||
|
<label for="replaceWithVariable">
|
||||||
|
{{ t("environment.replace_with_variable") }}</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<span class="flex space-x-2">
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:label="t('action.save')"
|
||||||
|
outline
|
||||||
|
@click="addEnvironment"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('action.cancel')"
|
||||||
|
outline
|
||||||
|
filled
|
||||||
|
@click="hideModal"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</HoppSmartModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
|
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||||
|
import {
|
||||||
|
addEnvironmentVariable,
|
||||||
|
addGlobalEnvVariable,
|
||||||
|
} from "~/newstore/environments"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
position: { top: number; left: number }
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
replaceWithVariable: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const hideModal = () => {
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
scope.value = {
|
||||||
|
type: "global",
|
||||||
|
}
|
||||||
|
name.value = ""
|
||||||
|
replaceWithVariable.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scope =
|
||||||
|
| {
|
||||||
|
type: "global"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "my-environment"
|
||||||
|
environment: Environment
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "team-environment"
|
||||||
|
environment: TeamEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = ref<Scope>({
|
||||||
|
type: "global",
|
||||||
|
})
|
||||||
|
|
||||||
|
const replaceWithVariable = ref(false)
|
||||||
|
|
||||||
|
const name = ref("")
|
||||||
|
|
||||||
|
const addEnvironment = async () => {
|
||||||
|
if (!name.value) {
|
||||||
|
toast.error(`${t("environment.invalid_name")}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (scope.value.type === "global") {
|
||||||
|
addGlobalEnvVariable({
|
||||||
|
key: name.value,
|
||||||
|
value: props.value,
|
||||||
|
})
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
} else if (scope.value.type === "my-environment") {
|
||||||
|
addEnvironmentVariable(scope.value.index, {
|
||||||
|
key: name.value,
|
||||||
|
value: props.value,
|
||||||
|
})
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
} else {
|
||||||
|
const newVariables = [
|
||||||
|
...scope.value.environment.environment.variables,
|
||||||
|
{
|
||||||
|
key: name.value,
|
||||||
|
value: props.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await pipe(
|
||||||
|
updateTeamEnvironment(
|
||||||
|
JSON.stringify(newVariables),
|
||||||
|
scope.value.environment.id,
|
||||||
|
scope.value.environment.environment.name
|
||||||
|
),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(`${getErrorMessage(err)}`)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
hideModal()
|
||||||
|
toast.success(`${t("environment.updated")}`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
|
if (replaceWithVariable.value) {
|
||||||
|
//replace the current tab endpoint with the variable name with << and >>
|
||||||
|
const variableName = `<<${name.value}>>`
|
||||||
|
//replace the currenttab endpoint containing the value in the text with variablename
|
||||||
|
currentActiveTab.value.document.request.endpoint =
|
||||||
|
currentActiveTab.value.document.request.endpoint.replace(
|
||||||
|
props.value,
|
||||||
|
variableName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
hideModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
|
if (err.type === "network_error") {
|
||||||
|
return t("error.network_error")
|
||||||
|
} else {
|
||||||
|
switch (err.error) {
|
||||||
|
case "team_environment/not_found":
|
||||||
|
return t("team_environment.not_found")
|
||||||
|
case "Forbidden resource":
|
||||||
|
return t("profile.no_permission")
|
||||||
|
default:
|
||||||
|
return t("error.something_went_wrong")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<span
|
<span
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="`${t('environment.select')}`"
|
:title="`${t('environment.select')}`"
|
||||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
class="bg-transparent select-wrapper"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconLayers"
|
:icon="IconLayers"
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
class="flex-1 !justify-start pr-8 rounded-none"
|
class="flex-1 !justify-start pr-8 rounded-none"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="tippyActions"
|
ref="tippyActions"
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isScopeSelector"
|
||||||
:label="`${t('environment.no_environment')}`"
|
:label="`${t('environment.no_environment')}`"
|
||||||
:info-icon="
|
:info-icon="
|
||||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||||
@@ -47,6 +49,21 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-else-if="isScopeSelector && modelValue"
|
||||||
|
:label="t('environment.global')"
|
||||||
|
:icon="IconGlobe"
|
||||||
|
:info-icon="modelValue.type === 'global' ? IconCheck : undefined"
|
||||||
|
:active-info-icon="modelValue.type === 'global'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
$emit('update:modelValue', {
|
||||||
|
type: 'global',
|
||||||
|
})
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<HoppSmartTabs
|
<HoppSmartTabs
|
||||||
v-model="selectedEnvTab"
|
v-model="selectedEnvTab"
|
||||||
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary"
|
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary"
|
||||||
@@ -61,11 +78,14 @@
|
|||||||
:key="`gen-${index}`"
|
:key="`gen-${index}`"
|
||||||
:icon="IconLayers"
|
:icon="IconLayers"
|
||||||
:label="gen.name"
|
:label="gen.name"
|
||||||
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
|
:info-icon="isEnvActive(index) ? IconCheck : undefined"
|
||||||
:active-info-icon="index === selectedEnv.index"
|
:active-info-icon="isEnvActive(index)"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
|
handleEnvironmentChange(index, {
|
||||||
|
type: 'my-environment',
|
||||||
|
environment: gen,
|
||||||
|
})
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -96,18 +116,14 @@
|
|||||||
:key="`gen-team-${index}`"
|
:key="`gen-team-${index}`"
|
||||||
:icon="IconLayers"
|
:icon="IconLayers"
|
||||||
:label="gen.environment.name"
|
:label="gen.environment.name"
|
||||||
:info-icon="
|
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
|
||||||
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
|
:active-info-icon="isEnvActive(gen.id)"
|
||||||
"
|
|
||||||
:active-info-icon="gen.id === selectedEnv.teamEnvID"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
selectedEnvironmentIndex = {
|
handleEnvironmentChange(index, {
|
||||||
type: 'TEAM_ENV',
|
type: 'team-environment',
|
||||||
teamEnvID: gen.id,
|
environment: gen,
|
||||||
teamID: gen.teamID,
|
})
|
||||||
environment: gen.environment,
|
|
||||||
}
|
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -136,9 +152,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, onMounted, ref, watch } from "vue"
|
||||||
import IconCheck from "~icons/lucide/check"
|
import IconCheck from "~icons/lucide/check"
|
||||||
import IconLayers from "~icons/lucide/layers"
|
import IconLayers from "~icons/lucide/layers"
|
||||||
|
import IconGlobe from "~icons/lucide/globe"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
@@ -156,6 +173,31 @@ import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
|||||||
import { useLocalState } from "~/newstore/localstate"
|
import { useLocalState } from "~/newstore/localstate"
|
||||||
import { onLoggedIn } from "~/composables/auth"
|
import { onLoggedIn } from "~/composables/auth"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
|
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||||
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
|
||||||
|
type Scope =
|
||||||
|
| {
|
||||||
|
type: "global"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "my-environment"
|
||||||
|
environment: Environment
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "team-environment"
|
||||||
|
environment: TeamEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
isScopeSelector?: boolean
|
||||||
|
modelValue?: Scope
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", data: Scope): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
const mdAndLarger = breakpoints.greater("md")
|
const mdAndLarger = breakpoints.greater("md")
|
||||||
@@ -170,6 +212,39 @@ const myEnvironments = useReadonlyStream(environments$, [])
|
|||||||
|
|
||||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||||
|
|
||||||
|
// TeamList-Adapter
|
||||||
|
const teamListAdapter = new TeamListAdapter(true)
|
||||||
|
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||||
|
const teamListFetched = ref(false)
|
||||||
|
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||||
|
|
||||||
|
onLoggedIn(() => {
|
||||||
|
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
||||||
|
})
|
||||||
|
|
||||||
|
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
|
||||||
|
REMEMBERED_TEAM_ID.value = team.id
|
||||||
|
changeWorkspace({
|
||||||
|
teamID: team.id,
|
||||||
|
teamName: team.name,
|
||||||
|
type: "team",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => myTeams.value,
|
||||||
|
(newTeams) => {
|
||||||
|
if (newTeams && !teamListFetched.value) {
|
||||||
|
teamListFetched.value = true
|
||||||
|
if (REMEMBERED_TEAM_ID.value) {
|
||||||
|
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
||||||
|
if (team) switchToTeamWorkspace(team)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TeamEnv List Adapter
|
||||||
const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
|
const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
|
||||||
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
|
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
|
||||||
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
|
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
|
||||||
@@ -204,39 +279,89 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// TeamList-Adapter
|
const handleEnvironmentChange = (
|
||||||
const teamListAdapter = new TeamListAdapter(true)
|
index: number,
|
||||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
env?:
|
||||||
const teamListFetched = ref(false)
|
| {
|
||||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
type: "my-environment"
|
||||||
|
environment: Environment
|
||||||
onLoggedIn(() => {
|
}
|
||||||
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
| {
|
||||||
})
|
type: "team-environment"
|
||||||
|
environment: TeamEnvironment
|
||||||
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
|
}
|
||||||
REMEMBERED_TEAM_ID.value = team.id
|
) => {
|
||||||
changeWorkspace({
|
if (props.isScopeSelector && env) {
|
||||||
teamID: team.id,
|
if (env.type === "my-environment") {
|
||||||
teamName: team.name,
|
emit("update:modelValue", {
|
||||||
type: "team",
|
type: "my-environment",
|
||||||
|
environment: env.environment,
|
||||||
|
index,
|
||||||
})
|
})
|
||||||
|
} else if (env.type === "team-environment") {
|
||||||
|
emit("update:modelValue", {
|
||||||
|
type: "team-environment",
|
||||||
|
environment: env.environment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (env && env.type === "my-environment") {
|
||||||
|
selectedEnvironmentIndex.value = {
|
||||||
|
type: "MY_ENV",
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
} else if (env && env.type === "team-environment") {
|
||||||
|
selectedEnvironmentIndex.value = {
|
||||||
|
type: "TEAM_ENV",
|
||||||
|
teamEnvID: env.environment.id,
|
||||||
|
teamID: env.environment.teamID,
|
||||||
|
environment: env.environment.environment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
const isEnvActive = (id: string | number) => {
|
||||||
() => myTeams.value,
|
if (props.isScopeSelector) {
|
||||||
(newTeams) => {
|
if (props.modelValue?.type === "my-environment") {
|
||||||
if (newTeams && !teamListFetched.value) {
|
return props.modelValue.index === id
|
||||||
teamListFetched.value = true
|
} else if (props.modelValue?.type === "team-environment") {
|
||||||
if (REMEMBERED_TEAM_ID.value) {
|
return (
|
||||||
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
props.modelValue?.type === "team-environment" &&
|
||||||
if (team) switchToTeamWorkspace(team)
|
props.modelValue.environment &&
|
||||||
|
props.modelValue.environment.id === id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
|
return selectedEnv.value.index === id
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||||
|
selectedEnv.value.teamEnvID === id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const selectedEnv = computed(() => {
|
const selectedEnv = computed(() => {
|
||||||
|
if (props.isScopeSelector) {
|
||||||
|
if (props.modelValue?.type === "my-environment") {
|
||||||
|
return {
|
||||||
|
type: "MY_ENV",
|
||||||
|
index: props.modelValue.index,
|
||||||
|
name: props.modelValue.environment?.name,
|
||||||
|
}
|
||||||
|
} else if (props.modelValue?.type === "team-environment") {
|
||||||
|
return {
|
||||||
|
type: "TEAM_ENV",
|
||||||
|
name: props.modelValue.environment.environment.name,
|
||||||
|
teamEnvID: props.modelValue.environment.id,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { type: "global", name: "Global" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
return {
|
return {
|
||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
@@ -262,6 +387,45 @@ const selectedEnv = computed(() => {
|
|||||||
} else {
|
} else {
|
||||||
return { type: "NO_ENV_SELECTED" }
|
return { type: "NO_ENV_SELECTED" }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the selected environment as initial scope value
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.isScopeSelector) {
|
||||||
|
if (
|
||||||
|
selectedEnvironmentIndex.value.type === "MY_ENV" &&
|
||||||
|
selectedEnvironmentIndex.value.index !== undefined
|
||||||
|
) {
|
||||||
|
emit("update:modelValue", {
|
||||||
|
type: "my-environment",
|
||||||
|
environment: myEnvironments.value[selectedEnvironmentIndex.value.index],
|
||||||
|
index: selectedEnvironmentIndex.value.index,
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||||
|
selectedEnvironmentIndex.value.teamEnvID &&
|
||||||
|
teamEnvironmentList.value &&
|
||||||
|
teamEnvironmentList.value.length > 0
|
||||||
|
) {
|
||||||
|
const teamEnv = teamEnvironmentList.value.find(
|
||||||
|
(env) =>
|
||||||
|
env.id ===
|
||||||
|
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||||
|
selectedEnvironmentIndex.value.teamEnvID)
|
||||||
|
)
|
||||||
|
if (teamEnv) {
|
||||||
|
emit("update:modelValue", {
|
||||||
|
type: "team-environment",
|
||||||
|
environment: teamEnv,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit("update:modelValue", {
|
||||||
|
type: "global",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
|
|||||||
@@ -26,6 +26,13 @@
|
|||||||
:editing-variable-name="editingVariableName"
|
:editing-variable-name="editingVariableName"
|
||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
|
<EnvironmentsAdd
|
||||||
|
:show="showModalNew"
|
||||||
|
:name="editingVariableName"
|
||||||
|
:value="editingVariableValue"
|
||||||
|
:position="position"
|
||||||
|
@hide-modal="displayModalNew(false)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -161,10 +168,18 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const showModalNew = ref(false)
|
||||||
const showModalDetails = ref(false)
|
const showModalDetails = ref(false)
|
||||||
const action = ref<"new" | "edit">("edit")
|
const action = ref<"new" | "edit">("edit")
|
||||||
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
||||||
const editingVariableName = ref("")
|
const editingVariableName = ref("")
|
||||||
|
const editingVariableValue = ref("")
|
||||||
|
|
||||||
|
const position = ref({ top: 0, left: 0 })
|
||||||
|
|
||||||
|
const displayModalNew = (shouldDisplay: boolean) => {
|
||||||
|
showModalNew.value = shouldDisplay
|
||||||
|
}
|
||||||
|
|
||||||
const displayModalEdit = (shouldDisplay: boolean) => {
|
const displayModalEdit = (shouldDisplay: boolean) => {
|
||||||
action.value = "edit"
|
action.value = "edit"
|
||||||
@@ -233,4 +248,10 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
defineActionHandler("modals.environment.add", ({ envName, variableName }) => {
|
||||||
|
editingVariableName.value = envName
|
||||||
|
editingVariableValue.value = variableName
|
||||||
|
displayModalNew(true)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ import { useReadonlyStream } from "@composables/stream"
|
|||||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import { onClickOutside } from "@vueuse/core"
|
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||||
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -149,6 +150,11 @@ const handleKeystroke = (ev: KeyboardEvent) => {
|
|||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ev.shiftKey) {
|
||||||
|
showSuggestionPopover.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
showSuggestionPopover.value = true
|
showSuggestionPopover.value = true
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -299,8 +305,46 @@ const envVars = computed(() =>
|
|||||||
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
||||||
|
|
||||||
const initView = (el: any) => {
|
const initView = (el: any) => {
|
||||||
|
function handleTextSelection() {
|
||||||
|
const selection = view.value?.state.selection.main
|
||||||
|
if (selection) {
|
||||||
|
const from = selection.from
|
||||||
|
const to = selection.to
|
||||||
|
const text = view.value?.state.doc.sliceString(from, to)
|
||||||
|
const { top, left } = view.value?.coordsAtPos(from)
|
||||||
|
if (text) {
|
||||||
|
invokeAction("contextmenu.open", {
|
||||||
|
position: {
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
},
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
showSuggestionPopover.value = false
|
||||||
|
} else {
|
||||||
|
invokeAction("contextmenu.open", {
|
||||||
|
position: {
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
},
|
||||||
|
text: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce to prevent double click from selecting the word
|
||||||
|
const debounceFn = useDebounceFn(() => {
|
||||||
|
handleTextSelection()
|
||||||
|
}, 140)
|
||||||
|
|
||||||
|
el.addEventListener("mouseup", debounceFn)
|
||||||
|
el.addEventListener("keyup", debounceFn)
|
||||||
|
|
||||||
const extensions: Extension = [
|
const extensions: Extension = [
|
||||||
|
EditorView.lineWrapping,
|
||||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
||||||
|
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (props.readonly) {
|
if (props.readonly) {
|
||||||
update.view.contentDOM.inputMode = "none"
|
update.view.contentDOM.inputMode = "none"
|
||||||
@@ -431,7 +475,7 @@ watch(editor, () => {
|
|||||||
@apply border-b border-x border-divider;
|
@apply border-b border-x border-divider;
|
||||||
@apply overflow-y-auto;
|
@apply overflow-y-auto;
|
||||||
@apply -left-[1px];
|
@apply -left-[1px];
|
||||||
@apply right-0;
|
@apply -right-[1px];
|
||||||
|
|
||||||
top: calc(100% + 1px);
|
top: calc(100% + 1px);
|
||||||
border-radius: 0 0 8px 8px;
|
border-radius: 0 0 8px 8px;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
class="flex flex-col flex-1"
|
class="flex flex-col flex-1"
|
||||||
>
|
>
|
||||||
<SmartTreeBranch
|
<SmartTreeBranch
|
||||||
|
:root-nodes-length="rootNodes.data.length"
|
||||||
:node-item="rootNode"
|
:node-item="rootNode"
|
||||||
:adapter="adapter as SmartTreeAdapter<T>"
|
:adapter="adapter as SmartTreeAdapter<T>"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -85,19 +85,25 @@ const props = defineProps<{
|
|||||||
* The node item that will be used to render the tree branch content
|
* The node item that will be used to render the tree branch content
|
||||||
*/
|
*/
|
||||||
nodeItem: TreeNode<T>
|
nodeItem: TreeNode<T>
|
||||||
|
/**
|
||||||
|
* Total number of rootNode
|
||||||
|
*/
|
||||||
|
rootNodesLength?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const CHILD_SLOT_NAME = "default"
|
const CHILD_SLOT_NAME = "default"
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
|
const isOnlyRootChild = computed(() => props.rootNodesLength === 1)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks whether the children on this branch were ever rendered
|
* Marks whether the children on this branch were ever rendered
|
||||||
* See the usage inside '<template>' for more info
|
* See the usage inside '<template>' for more info
|
||||||
*/
|
*/
|
||||||
const childrenRendered = ref(false)
|
const childrenRendered = ref(isOnlyRootChild.value)
|
||||||
|
|
||||||
const showChildren = ref(false)
|
const showChildren = ref(isOnlyRootChild.value)
|
||||||
const isNodeOpen = ref(false)
|
const isNodeOpen = ref(isOnlyRootChild.value)
|
||||||
|
|
||||||
const highlightNode = ref(false)
|
const highlightNode = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import {
|
|||||||
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment"
|
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment"
|
||||||
import xmlFormat from "xml-formatter"
|
import xmlFormat from "xml-formatter"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
import { useDebounceFn } from "@vueuse/core"
|
||||||
// TODO: Migrate from legacy mode
|
// TODO: Migrate from legacy mode
|
||||||
|
|
||||||
type ExtendedEditorConfig = {
|
type ExtendedEditorConfig = {
|
||||||
@@ -218,6 +220,40 @@ export function useCodemirror(
|
|||||||
ViewPlugin.fromClass(
|
ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
|
function handleTextSelection() {
|
||||||
|
const selection = view.value?.state.selection.main
|
||||||
|
if (selection) {
|
||||||
|
const from = selection.from
|
||||||
|
const to = selection.to
|
||||||
|
const text = view.value?.state.doc.sliceString(from, to)
|
||||||
|
const { top, left } = view.value?.coordsAtPos(from)
|
||||||
|
if (text) {
|
||||||
|
invokeAction("contextmenu.open", {
|
||||||
|
position: {
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
},
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
invokeAction("contextmenu.open", {
|
||||||
|
position: {
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
},
|
||||||
|
text: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce to prevent double click from selecting the word
|
||||||
|
const debounceFn = useDebounceFn(() => {
|
||||||
|
handleTextSelection()
|
||||||
|
}, 140)
|
||||||
|
|
||||||
|
el.addEventListener("mouseup", debounceFn)
|
||||||
|
el.addEventListener("keyup", debounceFn)
|
||||||
const cursorPos = update.state.selection.main.head
|
const cursorPos = update.state.selection.main.head
|
||||||
const line = update.state.doc.lineAt(cursorPos)
|
const line = update.state.doc.lineAt(cursorPos)
|
||||||
|
|
||||||
@@ -276,6 +312,7 @@ export function useCodemirror(
|
|||||||
run: indentLess,
|
run: indentLess,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||||
]
|
]
|
||||||
|
|
||||||
if (environmentTooltip) extensions.push(environmentTooltip.extension)
|
if (environmentTooltip) extensions.push(environmentTooltip.extension)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { HoppRESTDocument } from "./rest/document"
|
|||||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||||
|
|
||||||
export type HoppAction =
|
export type HoppAction =
|
||||||
|
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
||||||
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
||||||
| "request.reset" // Clear request data
|
| "request.reset" // Clear request data
|
||||||
| "request.copy-link" // Copy Request Link
|
| "request.copy-link" // Copy Request Link
|
||||||
@@ -24,6 +25,9 @@ export type HoppAction =
|
|||||||
| "modals.search.toggle" // Shows the search modal
|
| "modals.search.toggle" // Shows the search modal
|
||||||
| "modals.support.toggle" // Shows the support modal
|
| "modals.support.toggle" // Shows the support modal
|
||||||
| "modals.share.toggle" // Shows the share modal
|
| "modals.share.toggle" // Shows the share modal
|
||||||
|
| "modals.environment.add" // Show add environment modal via context menu
|
||||||
|
| "modals.my.environment.edit" // Edit current personal environment
|
||||||
|
| "modals.team.environment.edit" // Edit current team environment
|
||||||
| "navigation.jump.rest" // Jump to REST page
|
| "navigation.jump.rest" // Jump to REST page
|
||||||
| "navigation.jump.graphql" // Jump to GraphQL page
|
| "navigation.jump.graphql" // Jump to GraphQL page
|
||||||
| "navigation.jump.realtime" // Jump to realtime page
|
| "navigation.jump.realtime" // Jump to realtime page
|
||||||
@@ -54,6 +58,13 @@ export type HoppAction =
|
|||||||
* will know if you got something wrong if there is a type error in this file
|
* will know if you got something wrong if there is a type error in this file
|
||||||
*/
|
*/
|
||||||
type HoppActionArgsMap = {
|
type HoppActionArgsMap = {
|
||||||
|
"contextmenu.open": {
|
||||||
|
position: {
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
}
|
||||||
|
text: string | null
|
||||||
|
}
|
||||||
"modals.my.environment.edit": {
|
"modals.my.environment.edit": {
|
||||||
envName: string
|
envName: string
|
||||||
variableName: string
|
variableName: string
|
||||||
@@ -68,6 +79,10 @@ type HoppActionArgsMap = {
|
|||||||
"gql.request.open": {
|
"gql.request.open": {
|
||||||
request: HoppGQLRequest
|
request: HoppGQLRequest
|
||||||
}
|
}
|
||||||
|
"modals.environment.add": {
|
||||||
|
envName: string
|
||||||
|
variableName: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("shortcut.general.command_menu"),
|
label: t("shortcut.general.command_menu"),
|
||||||
keys: ["/"],
|
keys: [getPlatformSpecialKey(), "K"],
|
||||||
section: t("shortcut.general.title"),
|
section: t("shortcut.general.title"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("shortcut.general.show_all"),
|
label: t("shortcut.general.show_all"),
|
||||||
keys: [getPlatformSpecialKey(), "K"],
|
keys: [getPlatformSpecialKey(), "/"],
|
||||||
section: t("shortcut.general.title"),
|
section: t("shortcut.general.title"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -84,6 +84,13 @@
|
|||||||
:show="savingRequest"
|
:show="savingRequest"
|
||||||
@hide-modal="onSaveModalClose"
|
@hide-modal="onSaveModalClose"
|
||||||
/>
|
/>
|
||||||
|
<AppContextMenu
|
||||||
|
v-if="contextMenu.show"
|
||||||
|
:show="contextMenu.show"
|
||||||
|
:position="contextMenu.position"
|
||||||
|
:text="contextMenu.text"
|
||||||
|
@hide-modal="contextMenu.show = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -138,6 +145,24 @@ const reqName = ref<string>("")
|
|||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
type PopupDetails = {
|
||||||
|
show: boolean
|
||||||
|
position: {
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
}
|
||||||
|
text: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextMenu = ref<PopupDetails>({
|
||||||
|
show: false,
|
||||||
|
position: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
},
|
||||||
|
text: null,
|
||||||
|
})
|
||||||
|
|
||||||
const tabs = getActiveTabs()
|
const tabs = getActiveTabs()
|
||||||
|
|
||||||
const confirmSync = useReadonlyStream(currentSyncingStatus$, {
|
const confirmSync = useReadonlyStream(currentSyncingStatus$, {
|
||||||
@@ -365,6 +390,22 @@ function oAuthURL() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineActionHandler("contextmenu.open", ({ position, text }) => {
|
||||||
|
if (text) {
|
||||||
|
contextMenu.value = {
|
||||||
|
show: true,
|
||||||
|
position,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contextMenu.value = {
|
||||||
|
show: false,
|
||||||
|
position,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
setupTabStateSync()
|
setupTabStateSync()
|
||||||
bindRequestToURLParams()
|
bindRequestToURLParams()
|
||||||
oAuthURL()
|
oAuthURL()
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest"
|
||||||
|
import { ContextMenu, ContextMenuResult, ContextMenuService } from "../"
|
||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
|
||||||
|
const contextMenuResult: ContextMenuResult[] = [
|
||||||
|
{
|
||||||
|
id: "result1",
|
||||||
|
text: { type: "text", text: "Sample Text" },
|
||||||
|
icon: {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
action: () => {},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const testMenu: ContextMenu = {
|
||||||
|
menuID: "menu1",
|
||||||
|
getMenuFor: () => {
|
||||||
|
return {
|
||||||
|
results: contextMenuResult,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ContextMenuService", () => {
|
||||||
|
describe("registerMenu", () => {
|
||||||
|
it("should register a menu", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const service = container.bind(ContextMenuService)
|
||||||
|
|
||||||
|
service.registerMenu(testMenu)
|
||||||
|
|
||||||
|
const result = service.getMenuFor("text")
|
||||||
|
|
||||||
|
expect(result).toContainEqual(expect.objectContaining({ id: "result1" }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not register a menu twice", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const service = container.bind(ContextMenuService)
|
||||||
|
|
||||||
|
service.registerMenu(testMenu)
|
||||||
|
service.registerMenu(testMenu)
|
||||||
|
|
||||||
|
const result = service.getMenuFor("text")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should register multiple menus", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const service = container.bind(ContextMenuService)
|
||||||
|
|
||||||
|
const testMenu2: ContextMenu = {
|
||||||
|
menuID: "menu2",
|
||||||
|
getMenuFor: () => {
|
||||||
|
return {
|
||||||
|
results: contextMenuResult,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
service.registerMenu(testMenu)
|
||||||
|
service.registerMenu(testMenu2)
|
||||||
|
|
||||||
|
const result = service.getMenuFor("text")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getMenuFor", () => {
|
||||||
|
it("should get the menu", () => {
|
||||||
|
const sampleMenus = {
|
||||||
|
results: contextMenuResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = new TestContainer()
|
||||||
|
const service = container.bind(ContextMenuService)
|
||||||
|
|
||||||
|
service.registerMenu(testMenu)
|
||||||
|
|
||||||
|
const results = service.getMenuFor("sometext")
|
||||||
|
|
||||||
|
expect(results).toEqual(sampleMenus.results)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls registered menus with correct value", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const service = container.bind(ContextMenuService)
|
||||||
|
|
||||||
|
const testMenu2: ContextMenu = {
|
||||||
|
menuID: "some-id",
|
||||||
|
getMenuFor: vi.fn(() => ({
|
||||||
|
results: contextMenuResult,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
service.registerMenu(testMenu2)
|
||||||
|
|
||||||
|
service.getMenuFor("sometext")
|
||||||
|
|
||||||
|
expect(testMenu2.getMenuFor).toHaveBeenCalledWith("sometext")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return empty array if no menus are registered", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const service = container.bind(ContextMenuService)
|
||||||
|
|
||||||
|
const results = service.getMenuFor("sometext")
|
||||||
|
|
||||||
|
expect(results).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
109
packages/hoppscotch-common/src/services/context-menu/index.ts
Normal file
109
packages/hoppscotch-common/src/services/context-menu/index.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { Service } from "dioc"
|
||||||
|
import { Component } from "vue"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines how to render the text in a Context Menu Search Result
|
||||||
|
*/
|
||||||
|
export type ContextMenuTextType<T extends object | Component = never> =
|
||||||
|
| {
|
||||||
|
type: "text"
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "custom"
|
||||||
|
/**
|
||||||
|
* The component to render in place of the text
|
||||||
|
*/
|
||||||
|
component: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props to pass to the component
|
||||||
|
*/
|
||||||
|
componentProps: T extends Component<infer Props> ? Props : never
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines info about a context menu result so the UI can render it
|
||||||
|
*/
|
||||||
|
export interface ContextMenuResult {
|
||||||
|
/**
|
||||||
|
* The unique ID of the result
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* The text to render in the result
|
||||||
|
*/
|
||||||
|
text: ContextMenuTextType<any>
|
||||||
|
/**
|
||||||
|
* The icon to render as the signifier of the result
|
||||||
|
*/
|
||||||
|
icon: object | Component
|
||||||
|
/**
|
||||||
|
* The action to perform when the result is selected
|
||||||
|
*/
|
||||||
|
action: () => void
|
||||||
|
/**
|
||||||
|
* Additional metadata about the result
|
||||||
|
*/
|
||||||
|
meta?: {
|
||||||
|
/**
|
||||||
|
* The keyboard shortcut to trigger the result
|
||||||
|
*/
|
||||||
|
keyboardShortcut?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the state of a context menu
|
||||||
|
*/
|
||||||
|
export type ContextMenuState = {
|
||||||
|
results: ContextMenuResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a context menu
|
||||||
|
*/
|
||||||
|
export interface ContextMenu {
|
||||||
|
/**
|
||||||
|
* The unique ID of the context menu
|
||||||
|
* This is used to identify the context menu
|
||||||
|
*/
|
||||||
|
menuID: string
|
||||||
|
/**
|
||||||
|
* Gets the context menu for the given text
|
||||||
|
* @param text The text to get the context menu for
|
||||||
|
* @returns The context menu state
|
||||||
|
*/
|
||||||
|
getMenuFor: (text: string) => ContextMenuState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the context menu service
|
||||||
|
* This service is used to register context menus and get context menus for text
|
||||||
|
* This service is used by the context menu UI
|
||||||
|
*/
|
||||||
|
export class ContextMenuService extends Service {
|
||||||
|
public static readonly ID = "CONTEXT_MENU_SERVICE"
|
||||||
|
|
||||||
|
private menus: Map<string, ContextMenu> = new Map()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a menu with the context menu service
|
||||||
|
* @param menu The menu to register
|
||||||
|
*/
|
||||||
|
public registerMenu(menu: ContextMenu) {
|
||||||
|
this.menus.set(menu.menuID, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the context menu for the given text
|
||||||
|
* @param text The text to get the context menu for
|
||||||
|
*/
|
||||||
|
public getMenuFor(text: string): ContextMenuResult[] {
|
||||||
|
const menus = Array.from(this.menus.values()).map((x) => x.getMenuFor(text))
|
||||||
|
|
||||||
|
const result = menus.flatMap((x) => x.results)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { EnvironmentMenuService } from "../environment.menu"
|
||||||
|
import { ContextMenuService } from "../.."
|
||||||
|
|
||||||
|
vi.mock("~/modules/i18n", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
getI18n: () => (x: string) => x,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const actionsMock = vi.hoisted(() => ({
|
||||||
|
invokeAction: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("~/helpers/actions", async () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
invokeAction: actionsMock.invokeAction,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("EnvironmentMenuService", () => {
|
||||||
|
it("registers with the contextmenu service upon initialization", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const registerContextMenuFn = vi.fn()
|
||||||
|
|
||||||
|
container.bindMock(ContextMenuService, {
|
||||||
|
registerMenu: registerContextMenuFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const environment = container.bind(EnvironmentMenuService)
|
||||||
|
|
||||||
|
expect(registerContextMenuFn).toHaveBeenCalledOnce()
|
||||||
|
expect(registerContextMenuFn).toHaveBeenCalledWith(environment)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getMenuFor", () => {
|
||||||
|
it("should return a menu for adding environment", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const environment = container.bind(EnvironmentMenuService)
|
||||||
|
|
||||||
|
const test = "some-text"
|
||||||
|
const result = environment.getMenuFor(test)
|
||||||
|
|
||||||
|
expect(result.results).toContainEqual(
|
||||||
|
expect.objectContaining({ id: "environment" })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should invoke the add environment modal", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const environment = container.bind(EnvironmentMenuService)
|
||||||
|
|
||||||
|
const test = "some-text"
|
||||||
|
const result = environment.getMenuFor(test)
|
||||||
|
|
||||||
|
const action = result.results[0].action
|
||||||
|
action()
|
||||||
|
expect(actionsMock.invokeAction).toHaveBeenCalledOnce()
|
||||||
|
expect(actionsMock.invokeAction).toHaveBeenCalledWith(
|
||||||
|
"modals.environment.add",
|
||||||
|
{
|
||||||
|
envName: "test",
|
||||||
|
variableName: test,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { ContextMenuService } from "../.."
|
||||||
|
import { ParameterMenuService } from "../parameter.menu"
|
||||||
|
|
||||||
|
//regex containing both url and parameter
|
||||||
|
const urlAndParameterRegex = new RegExp("[^&?]*?=[^&?]*")
|
||||||
|
|
||||||
|
vi.mock("~/modules/i18n", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
getI18n: () => (x: string) => x,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const tabMock = vi.hoisted(() => ({
|
||||||
|
currentActiveTab: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("~/helpers/rest/tab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
currentActiveTab: tabMock.currentActiveTab,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("ParameterMenuService", () => {
|
||||||
|
it("registers with the contextmenu service upon initialization", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const registerContextMenuFn = vi.fn()
|
||||||
|
|
||||||
|
container.bindMock(ContextMenuService, {
|
||||||
|
registerMenu: registerContextMenuFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const parameter = container.bind(ParameterMenuService)
|
||||||
|
|
||||||
|
expect(registerContextMenuFn).toHaveBeenCalledOnce()
|
||||||
|
expect(registerContextMenuFn).toHaveBeenCalledWith(parameter)
|
||||||
|
|
||||||
|
describe("getMenuFor", () => {
|
||||||
|
it("validating if the text passes the regex and return the menu", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const parameter = container.bind(ParameterMenuService)
|
||||||
|
|
||||||
|
const test = "https://hoppscotch.io?id=some-text"
|
||||||
|
const result = parameter.getMenuFor(test)
|
||||||
|
|
||||||
|
if (test.match(urlAndParameterRegex)) {
|
||||||
|
expect(result.results).toContainEqual(
|
||||||
|
expect.objectContaining({ id: "parameter" })
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
expect(result.results).not.toContainEqual(
|
||||||
|
expect.objectContaining({ id: "parameter" })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call the addParameter function when action is called", () => {
|
||||||
|
const addParameterFn = vi.fn()
|
||||||
|
|
||||||
|
const container = new TestContainer()
|
||||||
|
const environment = container.bind(ParameterMenuService)
|
||||||
|
|
||||||
|
const test = "https://hoppscotch.io"
|
||||||
|
|
||||||
|
const result = environment.getMenuFor(test)
|
||||||
|
|
||||||
|
const action = result.results[0].action
|
||||||
|
|
||||||
|
action()
|
||||||
|
|
||||||
|
expect(addParameterFn).toHaveBeenCalledOnce()
|
||||||
|
expect(addParameterFn).toHaveBeenCalledWith(action)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call the extractParams function when addParameter function is called", () => {
|
||||||
|
const extractParamsFn = vi.fn()
|
||||||
|
|
||||||
|
const container = new TestContainer()
|
||||||
|
const environment = container.bind(ParameterMenuService)
|
||||||
|
|
||||||
|
const test = "https://hoppscotch.io"
|
||||||
|
|
||||||
|
const result = environment.getMenuFor(test)
|
||||||
|
|
||||||
|
const action = result.results[0].action
|
||||||
|
|
||||||
|
action()
|
||||||
|
|
||||||
|
expect(extractParamsFn).toHaveBeenCalledOnce()
|
||||||
|
expect(extractParamsFn).toHaveBeenCalledWith(action)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { ContextMenuService } from "../.."
|
||||||
|
import { URLMenuService } from "../url.menu"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
|
||||||
|
vi.mock("~/modules/i18n", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
getI18n: () => (x: string) => x,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const tabMock = vi.hoisted(() => ({
|
||||||
|
createNewTab: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("~/helpers/rest/tab", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
createNewTab: tabMock.createNewTab,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("URLMenuService", () => {
|
||||||
|
it("registers with the contextmenu service upon initialization", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const registerContextMenuFn = vi.fn()
|
||||||
|
|
||||||
|
container.bindMock(ContextMenuService, {
|
||||||
|
registerMenu: registerContextMenuFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const environment = container.bind(URLMenuService)
|
||||||
|
|
||||||
|
expect(registerContextMenuFn).toHaveBeenCalledOnce()
|
||||||
|
expect(registerContextMenuFn).toHaveBeenCalledWith(environment)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getMenuFor", () => {
|
||||||
|
it("validating if the text passes the regex and return the menu", () => {
|
||||||
|
function isValidURL(url: string) {
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to regular expression check
|
||||||
|
const pattern = /^(https?:\/\/)?([\w.-]+)(\.[\w.-]+)+([/?].*)?$/
|
||||||
|
return pattern.test(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = new TestContainer()
|
||||||
|
const url = container.bind(URLMenuService)
|
||||||
|
|
||||||
|
const test = ""
|
||||||
|
const result = url.getMenuFor(test)
|
||||||
|
|
||||||
|
if (isValidURL(test)) {
|
||||||
|
expect(result.results).toContainEqual(
|
||||||
|
expect.objectContaining({ id: "link-tab" })
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
expect(result).toEqual({ results: [] })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call the openNewTab function when action is called and a new hoppscotch tab is opened", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const url = container.bind(URLMenuService)
|
||||||
|
|
||||||
|
const test = "https://hoppscotch.io"
|
||||||
|
const result = url.getMenuFor(test)
|
||||||
|
|
||||||
|
result.results[0].action()
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: test,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tabMock.createNewTab).toHaveBeenCalledOnce()
|
||||||
|
expect(tabMock.createNewTab).toHaveBeenCalledWith({
|
||||||
|
request: request,
|
||||||
|
isDirty: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Service } from "dioc"
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuResult,
|
||||||
|
ContextMenuService,
|
||||||
|
ContextMenuState,
|
||||||
|
} from "../"
|
||||||
|
import { markRaw, ref } from "vue"
|
||||||
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
import IconPlusCircle from "~icons/lucide/plus-circle"
|
||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This menu returns a single result that allows the user
|
||||||
|
* to add the selected text as an environment variable
|
||||||
|
* This menus is shown on all text selections
|
||||||
|
*/
|
||||||
|
export class EnvironmentMenuService extends Service implements ContextMenu {
|
||||||
|
public static readonly ID = "ENVIRONMENT_CONTEXT_MENU_SERVICE"
|
||||||
|
|
||||||
|
private t = getI18n()
|
||||||
|
|
||||||
|
public readonly menuID = "environment"
|
||||||
|
|
||||||
|
private readonly contextMenu = this.bind(ContextMenuService)
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.contextMenu.registerMenu(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuFor(text: Readonly<string>): ContextMenuState {
|
||||||
|
const results = ref<ContextMenuResult[]>([])
|
||||||
|
results.value = [
|
||||||
|
{
|
||||||
|
id: "environment",
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: this.t("context_menu.set_environment_variable"),
|
||||||
|
},
|
||||||
|
icon: markRaw(IconPlusCircle),
|
||||||
|
action: () => {
|
||||||
|
invokeAction("modals.environment.add", {
|
||||||
|
envName: "test",
|
||||||
|
variableName: text,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const resultObj = <ContextMenuState>{
|
||||||
|
results: results.value,
|
||||||
|
}
|
||||||
|
return resultObj
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { Service } from "dioc"
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuResult,
|
||||||
|
ContextMenuService,
|
||||||
|
ContextMenuState,
|
||||||
|
} from "../"
|
||||||
|
import { markRaw, ref } from "vue"
|
||||||
|
import IconArrowDownRight from "~icons/lucide/arrow-down-right"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
|
|
||||||
|
//regex containing both url and parameter
|
||||||
|
const urlAndParameterRegex = new RegExp("[^&?]*?=[^&?]*")
|
||||||
|
|
||||||
|
interface Param {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The extracted parameters from the input
|
||||||
|
* with the new URL if it was provided
|
||||||
|
*/
|
||||||
|
interface ExtractedParams {
|
||||||
|
params: Param
|
||||||
|
newURL?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This menu returns a single result that allows the user
|
||||||
|
* to add the selected text as a parameter
|
||||||
|
* if the selected text is a valid URL
|
||||||
|
*/
|
||||||
|
export class ParameterMenuService extends Service implements ContextMenu {
|
||||||
|
public static readonly ID = "PARAMETER_CONTEXT_MENU_SERVICE"
|
||||||
|
|
||||||
|
private t = getI18n()
|
||||||
|
|
||||||
|
public readonly menuID = "parameter"
|
||||||
|
|
||||||
|
private readonly contextMenu = this.bind(ContextMenuService)
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.contextMenu.registerMenu(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param input The input to extract the parameters from
|
||||||
|
* @returns The extracted parameters and the new URL if it was provided
|
||||||
|
*/
|
||||||
|
private extractParams(input: string): ExtractedParams {
|
||||||
|
let text = input
|
||||||
|
let newURL: string | undefined
|
||||||
|
|
||||||
|
// if the input is a URL, extract the parameters
|
||||||
|
if (text.startsWith("http")) {
|
||||||
|
const url = new URL(text)
|
||||||
|
newURL = url.origin + url.pathname
|
||||||
|
text = url.search.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /(\w+)=(\w+)/g
|
||||||
|
const matches = text.matchAll(regex)
|
||||||
|
const params: Param = {}
|
||||||
|
|
||||||
|
// extract the parameters from the input
|
||||||
|
for (const match of matches) {
|
||||||
|
const [, key, value] = match
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { params, newURL }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the parameters from the input to the current request
|
||||||
|
* parameters and updates the endpoint if a new URL was provided
|
||||||
|
* @param text The input to extract the parameters from
|
||||||
|
*/
|
||||||
|
private addParameter(text: string) {
|
||||||
|
const { params, newURL } = this.extractParams(text)
|
||||||
|
|
||||||
|
const queryParams = []
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
queryParams.push({ key, value, active: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the parameters to the current request parameters
|
||||||
|
currentActiveTab.value.document.request.params = [
|
||||||
|
...currentActiveTab.value.document.request.params,
|
||||||
|
...queryParams,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (newURL) {
|
||||||
|
currentActiveTab.value.document.request.endpoint = newURL
|
||||||
|
} else {
|
||||||
|
// remove the parameter from the URL
|
||||||
|
const textRegex = new RegExp(`\\b${text.replace(/\?/g, "")}\\b`, "gi")
|
||||||
|
const sanitizedWord = currentActiveTab.value.document.request.endpoint
|
||||||
|
const newURL = sanitizedWord.replace(textRegex, "")
|
||||||
|
currentActiveTab.value.document.request.endpoint = newURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuFor(text: Readonly<string>): ContextMenuState {
|
||||||
|
const results = ref<ContextMenuResult[]>([])
|
||||||
|
|
||||||
|
if (urlAndParameterRegex.test(text)) {
|
||||||
|
results.value = [
|
||||||
|
{
|
||||||
|
id: "environment",
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: this.t("context_menu.add_parameter"),
|
||||||
|
},
|
||||||
|
icon: markRaw(IconArrowDownRight),
|
||||||
|
action: () => {
|
||||||
|
this.addParameter(text)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultObj = <ContextMenuState>{
|
||||||
|
results: results.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultObj
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Service } from "dioc"
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuResult,
|
||||||
|
ContextMenuService,
|
||||||
|
ContextMenuState,
|
||||||
|
} from ".."
|
||||||
|
import { markRaw, ref } from "vue"
|
||||||
|
import IconCopyPlus from "~icons/lucide/copy-plus"
|
||||||
|
import { createNewTab } from "~/helpers/rest/tab"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to check if a string is a valid URL
|
||||||
|
* @param url The string to check
|
||||||
|
* @returns Whether the string is a valid URL
|
||||||
|
*/
|
||||||
|
function isValidURL(url: string) {
|
||||||
|
try {
|
||||||
|
// Try to create a URL object
|
||||||
|
// this will fail for endpoints like "localhost:3000", ie without a protocol
|
||||||
|
new URL(url)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to regular expression check
|
||||||
|
const pattern = /^(https?:\/\/)?([\w.-]+)(\.[\w.-]+)+([/?].*)?$/
|
||||||
|
return pattern.test(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class URLMenuService extends Service implements ContextMenu {
|
||||||
|
public static readonly ID = "URL_CONTEXT_MENU_SERVICE"
|
||||||
|
|
||||||
|
private t = getI18n()
|
||||||
|
|
||||||
|
public readonly menuID = "url"
|
||||||
|
|
||||||
|
private readonly contextMenu = this.bind(ContextMenuService)
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.contextMenu.registerMenu(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a new tab with the provided URL
|
||||||
|
* @param url The URL to open
|
||||||
|
*/
|
||||||
|
private openNewTab(url: string) {
|
||||||
|
//create a new request object
|
||||||
|
const request = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: url,
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewTab({
|
||||||
|
request: request,
|
||||||
|
isDirty: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuFor(text: Readonly<string>): ContextMenuState {
|
||||||
|
const results = ref<ContextMenuResult[]>([])
|
||||||
|
|
||||||
|
if (isValidURL(text)) {
|
||||||
|
results.value = [
|
||||||
|
{
|
||||||
|
id: "link-tab",
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: this.t("context_menu.open_link_in_new_tab"),
|
||||||
|
},
|
||||||
|
icon: markRaw(IconCopyPlus),
|
||||||
|
action: () => {
|
||||||
|
this.openNewTab(text)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultObj = <ContextMenuState>{
|
||||||
|
results: results.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultObj
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"vite-plugin-html-config": "^1.0.10",
|
"vite-plugin-html-config": "^1.0.10",
|
||||||
"vite-plugin-inspect": "^0.7.4",
|
"vite-plugin-inspect": "^0.7.4",
|
||||||
"vite-plugin-pages": "^0.26.0",
|
"vite-plugin-pages": "^0.26.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.4.0",
|
"vite-plugin-pages-sitemap": "^1.4.5",
|
||||||
"vite-plugin-pwa": "^0.13.1",
|
"vite-plugin-pwa": "^0.13.1",
|
||||||
"vite-plugin-static-copy": "^0.12.0",
|
"vite-plugin-static-copy": "^0.12.0",
|
||||||
"vite-plugin-vue-layouts": "^0.7.0",
|
"vite-plugin-vue-layouts": "^0.7.0",
|
||||||
|
|||||||
@@ -78,15 +78,14 @@ export default defineConfig({
|
|||||||
routeStyle: "nuxt",
|
routeStyle: "nuxt",
|
||||||
dirs: "../hoppscotch-common/src/pages",
|
dirs: "../hoppscotch-common/src/pages",
|
||||||
importMode: "async",
|
importMode: "async",
|
||||||
onRoutesGenerated(routes) {
|
onRoutesGenerated: (routes) =>
|
||||||
return generateSitemap({
|
generateSitemap({
|
||||||
routes,
|
routes,
|
||||||
nuxtStyle: true,
|
nuxtStyle: true,
|
||||||
allowRobots: true,
|
allowRobots: true,
|
||||||
dest: ".sitemap-gen",
|
dest: ".sitemap-gen",
|
||||||
hostname: ENV.VITE_BASE_URL,
|
hostname: ENV.VITE_BASE_URL,
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
StaticCopy({
|
StaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@apply after:backface-hidden;
|
@apply after:backface-hidden;
|
||||||
@apply selection:bg-accentDark;
|
@apply selection:bg-accentDark;
|
||||||
@apply selection:text-accentContrast;
|
@apply selection:text-accentContrast;
|
||||||
|
@apply overscroll-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||||
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||||
"@histoire/plugin-vue": "^0.12.4",
|
"@histoire/plugin-vue": "^0.12.4",
|
||||||
"@iconify-json/lucide": "^1.1.40",
|
"@iconify-json/lucide": "^1.1.109",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
||||||
"@rushstack/eslint-patch": "^1.1.4",
|
"@rushstack/eslint-patch": "^1.1.4",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"vite-plugin-html-config": "^1.0.10",
|
"vite-plugin-html-config": "^1.0.10",
|
||||||
"vite-plugin-inspect": "^0.7.4",
|
"vite-plugin-inspect": "^0.7.4",
|
||||||
"vite-plugin-pages": "^0.26.0",
|
"vite-plugin-pages": "^0.26.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.4.0",
|
"vite-plugin-pages-sitemap": "^1.4.5",
|
||||||
"vite-plugin-pwa": "^0.13.1",
|
"vite-plugin-pwa": "^0.13.1",
|
||||||
"vite-plugin-vue-layouts": "^0.7.0",
|
"vite-plugin-vue-layouts": "^0.7.0",
|
||||||
"vite-plugin-windicss": "^1.8.8",
|
"vite-plugin-windicss": "^1.8.8",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@apply after:backface-hidden;
|
@apply after:backface-hidden;
|
||||||
@apply selection:bg-accentDark;
|
@apply selection:bg-accentDark;
|
||||||
@apply selection:text-accentContrast;
|
@apply selection:text-accentContrast;
|
||||||
|
@apply overscroll-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto divide-x divide-dividerLight bg-primaryLight tabs group-tabs"
|
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto divide-x divide-dividerLight bg-primaryLight tabs group-tabs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto"
|
class="flex flex-1 flex-shrink-0 w-0 overflow-hidden"
|
||||||
ref="scrollContainer"
|
ref="scrollContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -455,6 +455,7 @@ $slider-height: 4px;
|
|||||||
@apply min-w-0;
|
@apply min-w-0;
|
||||||
@apply bg-dividerDark;
|
@apply bg-dividerDark;
|
||||||
@apply hover:bg-secondaryLight;
|
@apply hover:bg-secondaryLight;
|
||||||
|
@apply active:bg-secondaryLight;
|
||||||
|
|
||||||
width: var(--thumb-width);
|
width: var(--thumb-width);
|
||||||
height: $slider-height;
|
height: $slider-height;
|
||||||
@@ -465,6 +466,7 @@ $slider-height: 4px;
|
|||||||
@apply min-w-0;
|
@apply min-w-0;
|
||||||
@apply bg-dividerDark;
|
@apply bg-dividerDark;
|
||||||
@apply hover:bg-secondaryLight;
|
@apply hover:bg-secondaryLight;
|
||||||
|
@apply active:bg-secondaryLight;
|
||||||
|
|
||||||
width: var(--thumb-width);
|
width: var(--thumb-width);
|
||||||
height: $slider-height;
|
height: $slider-height;
|
||||||
|
|||||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@@ -575,9 +575,6 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: ^3.2.25
|
specifier: ^3.2.25
|
||||||
version: 3.2.37
|
version: 3.2.37
|
||||||
vue-github-button:
|
|
||||||
specifier: ^3.0.3
|
|
||||||
version: 3.0.3
|
|
||||||
vue-i18n:
|
vue-i18n:
|
||||||
specifier: ^9.2.2
|
specifier: ^9.2.2
|
||||||
version: 9.2.2(vue@3.2.37)
|
version: 9.2.2(vue@3.2.37)
|
||||||
@@ -637,8 +634,8 @@ importers:
|
|||||||
specifier: ^3.1.1
|
specifier: ^3.1.1
|
||||||
version: 3.1.1(graphql@15.8.0)
|
version: 3.1.1(graphql@15.8.0)
|
||||||
'@iconify-json/lucide':
|
'@iconify-json/lucide':
|
||||||
specifier: ^1.1.40
|
specifier: ^1.1.109
|
||||||
version: 1.1.40
|
version: 1.1.109
|
||||||
'@intlify/vite-plugin-vue-i18n':
|
'@intlify/vite-plugin-vue-i18n':
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0(vite@3.1.4)(vue-i18n@9.2.2)
|
version: 7.0.0(vite@3.1.4)(vue-i18n@9.2.2)
|
||||||
@@ -748,8 +745,8 @@ importers:
|
|||||||
specifier: ^0.26.0
|
specifier: ^0.26.0
|
||||||
version: 0.26.0(@vue/compiler-sfc@3.2.39)(vite@3.1.4)
|
version: 0.26.0(@vue/compiler-sfc@3.2.39)(vite@3.1.4)
|
||||||
vite-plugin-pages-sitemap:
|
vite-plugin-pages-sitemap:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.5
|
||||||
version: 1.4.0
|
version: 1.4.5
|
||||||
vite-plugin-pwa:
|
vite-plugin-pwa:
|
||||||
specifier: ^0.13.1
|
specifier: ^0.13.1
|
||||||
version: 0.13.1(vite@3.1.4)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
version: 0.13.1(vite@3.1.4)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
||||||
@@ -985,8 +982,8 @@ importers:
|
|||||||
specifier: ^0.26.0
|
specifier: ^0.26.0
|
||||||
version: 0.26.0(@vue/compiler-sfc@3.2.45)(vite@3.2.4)
|
version: 0.26.0(@vue/compiler-sfc@3.2.45)(vite@3.2.4)
|
||||||
vite-plugin-pages-sitemap:
|
vite-plugin-pages-sitemap:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.5
|
||||||
version: 1.4.0
|
version: 1.4.5
|
||||||
vite-plugin-pwa:
|
vite-plugin-pwa:
|
||||||
specifier: ^0.13.1
|
specifier: ^0.13.1
|
||||||
version: 0.13.1(vite@3.2.4)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
version: 0.13.1(vite@3.2.4)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
||||||
@@ -1239,8 +1236,8 @@ importers:
|
|||||||
specifier: ^0.12.4
|
specifier: ^0.12.4
|
||||||
version: 0.12.4(histoire@0.12.4)(vite@3.2.4)(vue@3.2.45)
|
version: 0.12.4(histoire@0.12.4)(vite@3.2.4)(vue@3.2.45)
|
||||||
'@iconify-json/lucide':
|
'@iconify-json/lucide':
|
||||||
specifier: ^1.1.40
|
specifier: ^1.1.109
|
||||||
version: 1.1.40
|
version: 1.1.109
|
||||||
'@intlify/vite-plugin-vue-i18n':
|
'@intlify/vite-plugin-vue-i18n':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1(vite@3.2.4)
|
version: 6.0.1(vite@3.2.4)
|
||||||
@@ -1326,8 +1323,8 @@ importers:
|
|||||||
specifier: ^0.26.0
|
specifier: ^0.26.0
|
||||||
version: 0.26.0(@vue/compiler-sfc@3.2.45)(vite@3.2.4)
|
version: 0.26.0(@vue/compiler-sfc@3.2.45)(vite@3.2.4)
|
||||||
vite-plugin-pages-sitemap:
|
vite-plugin-pages-sitemap:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.5
|
||||||
version: 1.4.0
|
version: 1.4.5
|
||||||
vite-plugin-pwa:
|
vite-plugin-pwa:
|
||||||
specifier: ^0.13.1
|
specifier: ^0.13.1
|
||||||
version: 0.13.1(vite@3.2.4)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
version: 0.13.1(vite@3.2.4)(workbox-build@6.5.4)(workbox-window@6.5.4)
|
||||||
@@ -2942,7 +2939,7 @@ packages:
|
|||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.18.6
|
'@babel/code-frame': 7.18.6
|
||||||
'@babel/parser': 7.20.15
|
'@babel/parser': 7.22.5
|
||||||
'@babel/types': 7.20.7
|
'@babel/types': 7.20.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -5786,10 +5783,10 @@ packages:
|
|||||||
/@iarna/toml@2.2.5:
|
/@iarna/toml@2.2.5:
|
||||||
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
|
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
|
||||||
|
|
||||||
/@iconify-json/lucide@1.1.40:
|
/@iconify-json/lucide@1.1.109:
|
||||||
resolution: {integrity: sha512-4GeQtaiv3mJ+b0sn/c2KL8Tgf4XQvsX1AHDOseuGRhgoLCWG+ZdNRFxF5sp1I6T/VcQccegLPOp5XHn3NC1mmA==}
|
resolution: {integrity: sha512-1+zYieiKUAjN1x66kvcRmmtgBJaDbD7i4To8mhB6+3bEm/i61un76nspJ45LOSGovzBMvYZFIJpqJrGMipWPzw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 1.1.0
|
'@iconify/types': 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@iconify/types@1.1.0:
|
/@iconify/types@1.1.0:
|
||||||
@@ -5836,8 +5833,8 @@ packages:
|
|||||||
vue-i18n:
|
vue-i18n:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/message-compiler': 9.3.0-beta.24
|
'@intlify/message-compiler': 9.3.0-beta.25
|
||||||
'@intlify/shared': 9.3.0-beta.24
|
'@intlify/shared': 9.3.0-beta.25
|
||||||
jsonc-eslint-parser: 1.4.1
|
jsonc-eslint-parser: 1.4.1
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
vue-i18n: 9.2.2(vue@3.2.37)
|
vue-i18n: 9.2.2(vue@3.2.37)
|
||||||
@@ -5898,11 +5895,11 @@ packages:
|
|||||||
source-map-js: 1.0.2
|
source-map-js: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@intlify/message-compiler@9.3.0-beta.24:
|
/@intlify/message-compiler@9.3.0-beta.25:
|
||||||
resolution: {integrity: sha512-prhHATkgp0mpPqoVgiAtLmUc1JMvs8fMH6w53AVEBn+VF87dLhzanfmWY5FoZWORG51ag54gBDBOoM/VFv3m3A==}
|
resolution: {integrity: sha512-uT7ybqKoDEw1XITQYnTYjWgZnpCDmHv9e3D4MmJDqHl2qCm6anzdUXWKHUhqR87Ha9Z8Rl44v40iSI/4NUbppQ==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/shared': 9.3.0-beta.24
|
'@intlify/shared': 9.3.0-beta.25
|
||||||
source-map-js: 1.0.2
|
source-map-js: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -5915,8 +5912,8 @@ packages:
|
|||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@intlify/shared@9.3.0-beta.24:
|
/@intlify/shared@9.3.0-beta.25:
|
||||||
resolution: {integrity: sha512-AKxJ8s7eKIQWkNaf4wyyoLRwf4puCuQgjSChlDJm5JBEt6T8HGgnYTJLRXu6LD/JACn3Qwu6hM/XRX1c9yvjmQ==}
|
resolution: {integrity: sha512-Zg+ECV9RPdp227tCJOgvPb+S3i651nf4kKHsMojSyWCppVK/4NFuDrBG2lIQSQL6Iq5LKVr5MkezHCW2NBTQRg==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -5936,7 +5933,7 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/bundle-utils': 7.0.0
|
'@intlify/bundle-utils': 7.0.0
|
||||||
'@intlify/shared': 9.3.0-beta.24
|
'@intlify/shared': 9.3.0-beta.25
|
||||||
'@rollup/pluginutils': 4.2.1
|
'@rollup/pluginutils': 4.2.1
|
||||||
debug: 4.3.4(supports-color@9.2.2)
|
debug: 4.3.4(supports-color@9.2.2)
|
||||||
fast-glob: 3.2.11
|
fast-glob: 3.2.11
|
||||||
@@ -5963,7 +5960,7 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/bundle-utils': 3.4.0(vue-i18n@9.2.2)
|
'@intlify/bundle-utils': 3.4.0(vue-i18n@9.2.2)
|
||||||
'@intlify/shared': 9.3.0-beta.24
|
'@intlify/shared': 9.3.0-beta.25
|
||||||
'@rollup/pluginutils': 4.2.1
|
'@rollup/pluginutils': 4.2.1
|
||||||
debug: 4.3.4(supports-color@9.2.2)
|
debug: 4.3.4(supports-color@9.2.2)
|
||||||
fast-glob: 3.2.12
|
fast-glob: 3.2.12
|
||||||
@@ -5991,7 +5988,7 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/bundle-utils': 3.4.0(vue-i18n@9.2.2)
|
'@intlify/bundle-utils': 3.4.0(vue-i18n@9.2.2)
|
||||||
'@intlify/shared': 9.3.0-beta.24
|
'@intlify/shared': 9.3.0-beta.25
|
||||||
'@rollup/pluginutils': 4.2.1
|
'@rollup/pluginutils': 4.2.1
|
||||||
debug: 4.3.4(supports-color@9.2.2)
|
debug: 4.3.4(supports-color@9.2.2)
|
||||||
fast-glob: 3.2.12
|
fast-glob: 3.2.12
|
||||||
@@ -21478,8 +21475,8 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vite-plugin-pages-sitemap@1.4.0:
|
/vite-plugin-pages-sitemap@1.4.5:
|
||||||
resolution: {integrity: sha512-8GlmNSeObvDVUdmgutfWnGBZGsn3Zb8IXCjRODFB0tOhQqAQPZRwXrFha6ZLjvF4DgVJI3sW2FfiXa1wFxJvmg==}
|
resolution: {integrity: sha512-AcEoJ+0D0P1CwR1LjzBznHs6yNQVP7ha7l6cl/VHrHFcQXbVyKc+QOmLi1a3eONy+aDA3K01pZVK8TzTIufq/w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vite-plugin-pages@0.26.0(@vue/compiler-sfc@3.2.39)(vite@3.1.4):
|
/vite-plugin-pages@0.26.0(@vue/compiler-sfc@3.2.39)(vite@3.1.4):
|
||||||
|
|||||||
Reference in New Issue
Block a user