Compare commits

...

36 Commits

Author SHA1 Message Date
Balu Babu
58782fd8d8 chore: fixed issues with pnpm-lock file 2023-08-03 18:52:12 +05:30
Balu Babu
42cb728577 chore: resolved merge conflicts 2023-08-03 18:01:12 +05:30
Balu Babu
b9f36d04a8 chore: modified error code for magic-link provider check 2023-08-03 17:14:57 +05:30
Mir Arif Hasan
1704cc2bad chore: env check func moved to utils file 2023-08-03 17:40:02 +06:00
Mir Arif Hasan
ab9775d55f feat: feedback applied 2023-08-03 16:40:00 +06:00
Ankit Sridhar
88f6a4ae26 [feat] : Allow admins to revoke a team invite (HBE-230) (#3162)
feat: added functionality for admin to revoke team invite
2023-08-03 14:08:32 +05:30
Ankit Sridhar
af7ff9dc17 Merge branch 'main' into feat/conditional-auth-select 2023-08-03 11:42:17 +05:30
Anwarul Islam
610538ca02 chore: type and ux improvement for SmartTree (#3126) 2023-08-02 20:54:02 +05:30
Nivedin
8970ff5c68 feat: context menu (#3180)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-02 20:52:16 +05:30
Liyas Thomas
d1a564d5b8 fix: elastic overscroll on safari (#3221) 2023-08-02 20:47:54 +05:30
Mir Arif Hasan
cf29c29d16 chore: check added if ALLOWED_AUTH_PROVIDERS is there in the env file or not 2023-08-01 20:45:55 +06:00
Anwarul Islam
8bb1d19c07 fix: firefox browser scrollbar issue (#3201) 2023-08-01 13:20:17 +05:30
Mir Arif Hasan
c3d03d7162 fix: provider return type in SSO guards 2023-07-29 15:02:54 +06:00
Mir Arif Hasan
a72b1feda5 chore: handled internal server error for missing auth providers 2023-07-28 18:26:42 +06:00
Mir Arif Hasan
bdab2bb2d0 chore: auth provider name read from enum 2023-07-28 18:25:45 +06:00
Mir Arif Hasan
e50b3ddcc4 chore: removed unused imports 2023-07-27 19:01:27 +06:00
Mir Arif Hasan
8d59a69f48 feat: remove EmptyClassProvider class 2023-07-27 18:56:19 +06:00
Liyas Thomas
c1efa381f0 feat: svg badge asset (#3196) 2023-07-25 20:45:22 +05:30
Balu Babu
d090585685 chore: fixed mistake in AUTH_PROVIDER_NOT_SPECIFIED error description 2023-07-21 16:19:48 +05:30
Balu Babu
7b590fa57d chore: changed target of hoppscotch-backend service back to prod in docker.compose file 2023-07-21 16:18:11 +05:30
Balu Babu
83536ad715 chore: added comments to authProviderCheck function in auth/helper.ts 2023-07-21 16:17:22 +05:30
Balu Babu
197b48182f feat: magic-link can now be conditionally provisioned 2023-07-21 16:05:27 +05:30
Balu Babu
48356cb589 feat: social auth providers can now be conditionally provisioned 2023-07-21 15:57:53 +05:30
Andrew Bastin
29171d1b6f fix: generate-ui failing to build 2023-07-18 22:27:37 +05:30
Andrew Bastin
e869d49e16 chore: run tests on and against release branches 2023-07-18 21:46:38 +05:30
Andrew Bastin
6496bea846 chore: bump version to 2023.4.8 2023-07-18 21:46:36 +05:30
NicklasWallgren
39842559b5 fix: reduce the memory consumption during build to prevent OOM (#3148)
Co-authored-by: Nicklas Wallgren <nicklas.wallgren@folksam.se>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-07-18 00:08:06 +05:30
Anwarul Islam
51efb35aa6 fix: keybinding modifier issue (#3163) 2023-07-17 23:56:08 +05:30
NicklasWallgren
9402bb9285 fix: add healthcheck for db and remove unwanted volumes (#3150) 2023-07-17 21:22:56 +05:30
Liyas Thomas
5a516f7242 docs: fixed shortcut keys for spotlight and shortcuts menu (#3192) 2023-07-17 19:27:49 +05:30
Liyas Thomas
3b217d78e7 fix: deps mismatch for vite-plugin-pages-sitemap (#3191) 2023-07-17 19:26:43 +05:30
Liyas Thomas
8e153b38dc redesigned search button (#3187) 2023-07-17 14:40:14 +05:30
Akash K
6f38bfb148 chore: update generateSitemap usage (#3182) 2023-07-17 14:39:32 +05:30
Balu Babu
82b6e08d68 fix: fixed issue in team-environment test cases (#3189) 2023-07-17 12:33:11 +05:30
Anwarul Islam
25177bd635 fix: update vite-plugin-dts version which fixes build issue on docker/alpine (#3179) 2023-07-17 12:32:25 +05:30
5idereal
6928eb7992 feat(lang): update tw translation (#3170) 2023-07-14 11:36:08 +05:30
59 changed files with 2012 additions and 436 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
*/**/node_modules

View File

@@ -13,6 +13,7 @@ SESSION_SECRET='add some secret here'
# Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000"
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
# Google Auth Config
GOOGLE_CLIENT_ID="************************************************"

View File

@@ -2,9 +2,9 @@ name: Node.js CI
on:
push:
branches: [main, staging]
branches: [main, staging, "release/**"]
pull_request:
branches: [main, staging]
branches: [main, staging, "release/**"]
jobs:
test:

View File

@@ -19,10 +19,12 @@ services:
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
- PORT=3000
volumes:
- ./packages/hoppscotch-backend/:/usr/src/app
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
# - ./packages/hoppscotch-backend/:/usr/src/app
- /usr/src/app/node_modules/
depends_on:
- hoppscotch-db
hoppscotch-db:
condition: service_healthy
ports:
- "3170:3000"
@@ -60,12 +62,20 @@ services:
# you are using an external postgres instance
# This will be exposed at port 5432
hoppscotch-db:
image: postgres
image: postgres:15
ports:
- "5432:5432"
user: postgres
environment:
# The default user defined by the docker image
POSTGRES_USER: postgres
# NOTE: Please UPDATE THIS PASSWORD!
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch
healthcheck:
test: ["CMD-SHELL", "sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"]
interval: 5s
timeout: 5s
retries: 10

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.4.7",
"version": "2023.4.8",
"description": "",
"author": "",
"private": true,

View File

@@ -411,6 +411,23 @@ export class AdminResolver {
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 */
@Subscription(() => InvitedUser, {

View File

@@ -11,6 +11,7 @@ import {
INVALID_EMAIL,
ONLY_ONE_ADMIN_ACCOUNT,
TEAM_INVITE_ALREADY_MEMBER,
TEAM_INVITE_NO_INVITE_FOUND,
USER_ALREADY_INVITED,
USER_IS_ADMIN,
USER_NOT_FOUND,
@@ -416,4 +417,19 @@ export class AdminService {
if (E.isLeft(team)) return E.left(team.left);
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);
}
}

View File

@@ -2,9 +2,9 @@ import {
Body,
Controller,
Get,
InternalServerErrorException,
Post,
Query,
Req,
Request,
Res,
UseGuards,
@@ -19,12 +19,18 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { GqlUser } from 'src/decorators/gql-user.decorator';
import { AuthUser } from 'src/types/AuthUser';
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 { GithubSSOGuard } from './guards/github-sso.guard';
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
import { SkipThrottle } from '@nestjs/throttler';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'auth', version: '1' })
@@ -39,6 +45,9 @@ export class AuthController {
@Body() authData: SignInMagicDto,
@Query('origin') origin: string,
) {
if (!authProviderCheck(AuthProvider.EMAIL))
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
const deviceIdToken = await this.authService.signInMagicLink(
authData.email,
origin,

View File

@@ -11,6 +11,7 @@ import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
import { GoogleStrategy } from './strategies/google.strategy';
import { GithubStrategy } from './strategies/github.strategy';
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
import { AuthProvider, authProviderCheck } from './helper';
@Module({
imports: [
@@ -26,9 +27,9 @@ import { MicrosoftStrategy } from './strategies/microsoft.strategy';
AuthService,
JwtStrategy,
RTJwtStrategy,
GoogleStrategy,
GithubStrategy,
MicrosoftStrategy,
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
],
controllers: [AuthController],
})

View File

@@ -1,8 +1,20 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
@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) {
const req = context.switchToHttp().getRequest();

View File

@@ -1,8 +1,20 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
@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) {
const req = context.switchToHttp().getRequest();

View File

@@ -1,8 +1,26 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
@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) {
const req = context.switchToHttp().getRequest();

View File

@@ -1,10 +1,11 @@
import { ForbiddenException, HttpException, HttpStatus } from '@nestjs/common';
import { HttpException, HttpStatus } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AuthError } from 'src/types/AuthError';
import { AuthTokens } from 'src/types/AuthTokens';
import { Response } from 'express';
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 {
ACCESS_TOKEN = 'access_token',
@@ -16,6 +17,13 @@ export enum Origin {
APP = 'app',
}
export enum AuthProvider {
GOOGLE = 'GOOGLE',
GITHUB = 'GITHUB',
MICROSOFT = 'MICROSOFT',
EMAIL = 'EMAIL',
}
/**
* This function allows throw to be used as an expression
* @param errMessage Message present in the error message
@@ -97,3 +105,25 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
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;
}

View File

@@ -22,6 +22,30 @@ export const AUTH_FAIL = 'auth/fail';
*/
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.
* (FirebaseService)

View File

@@ -5,11 +5,14 @@ import * as cookieParser from 'cookie-parser';
import { VersioningType } from '@nestjs/common';
import * as session from 'express-session';
import { emitGQLSchemaFile } from './gql-schema';
import { checkEnvironmentAuthProvider } from './utils';
async function bootstrap() {
console.log(`Running in production: ${process.env.PRODUCTION}`);
console.log(`Port: ${process.env.PORT}`);
checkEnvironmentAuthProvider();
const app = await NestFactory.create(AppModule);
app.use(

View File

@@ -306,8 +306,8 @@ describe('TeamEnvironmentsService', () => {
);
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
...teamEnvironment,
id: 'newid',
...teamEnvironment,
});
const result = await teamEnvironmentsService.createDuplicateEnvironment(
@@ -337,8 +337,8 @@ describe('TeamEnvironmentsService', () => {
);
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
...teamEnvironment,
id: 'newid',
...teamEnvironment,
});
const result = await teamEnvironmentsService.createDuplicateEnvironment(

View File

@@ -9,7 +9,8 @@ import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array';
import { TeamMemberRole } from './team/team.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.
@@ -152,3 +153,31 @@ export function isValidLength(title: string, length: number) {
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);
}
}
}

View File

@@ -4,6 +4,7 @@
@apply after:backface-hidden;
@apply selection:bg-accentDark;
@apply selection:text-accentContrast;
@apply overscroll-none;
}
:root {

View File

@@ -151,6 +151,11 @@
"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."
},
"context_menu": {
"set_environment_variable": "Set as variable",
"add_parameter": "Add to parameter",
"open_link_in_new_tab": "Open link in new tab"
},
"count": {
"header": "Header {count}",
"message": "Message {count}",
@@ -195,16 +200,23 @@
"created": "Environment created",
"deleted": "Environment deletion",
"edit": "Edit Environment",
"global": "Global",
"invalid_name": "Please provide a name for the environment",
"my_environments": "My Environments",
"name": "Name",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment",
"no_environment": "No environment",
"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",
"set_as_environment": "Set as environment",
"team_environments": "Team Environments",
"title": "Environments",
"updated": "Environment updated",
"value": "Value",
"variable": "Variable",
"variable_list": "Variable List"
},
"error": {

View File

@@ -19,7 +19,7 @@
"edit": "編輯",
"filter": "篩選回應",
"go_back": "返回",
"go_forward": "Go forward",
"go_forward": "向前",
"group_by": "分組方式",
"label": "標籤",
"learn_more": "瞭解更多",
@@ -117,37 +117,37 @@
"username": "使用者名稱"
},
"collection": {
"created": "合已建立",
"different_parent": "Cannot reorder collection with different parent",
"edit": "編輯合",
"invalid_name": "請提供有效的合名稱",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "我的合",
"name": "我的新合",
"name_length_insufficient": "合名稱至少要有 3 個字元。",
"new": "建立合",
"order_changed": "Collection Order Updated",
"renamed": "合已重新命名",
"created": "合已建立",
"different_parent": "無法為父集合不同的集合重新排序",
"edit": "編輯合",
"invalid_name": "請提供有效的合名稱",
"invalid_root_move": "集合已在根目錄",
"moved": "移動成功",
"my_collections": "我的合",
"name": "我的新合",
"name_length_insufficient": "合名稱至少要有 3 個字元。",
"new": "建立合",
"order_changed": "集合順序已更新",
"renamed": "合已重新命名",
"request_in_use": "請求正在使用中",
"save_as": "另存為",
"select": "選擇一個合",
"select": "選擇一個合",
"select_location": "選擇位置",
"select_team": "選擇一個團隊",
"team_collections": "團隊合"
"team_collections": "團隊合"
},
"confirm": {
"exit_team": "您確定要離開此團隊嗎?",
"logout": "您確定要登出嗎?",
"remove_collection": "您確定要永久刪除該合嗎?",
"remove_collection": "您確定要永久刪除該合嗎?",
"remove_environment": "您確定要永久刪除該環境嗎?",
"remove_folder": "您確定要永久刪除該資料夾嗎?",
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
"remove_request": "您確定要永久刪除該請求嗎?",
"remove_team": "您確定要刪除該團隊嗎?",
"remove_telemetry": "您確定要退出遙測服務嗎?",
"request_change": "您確定要捨棄當前請求嗎?未儲存的變更將遺失。",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"request_change": "您確定要捨棄目前的請求嗎?未儲存的變更將遺失。",
"save_unsaved_tab": "您要儲存在此分頁做出的改動嗎?",
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
},
"count": {
@@ -160,13 +160,13 @@
},
"documentation": {
"generate": "產生文件",
"generate_message": "匯入 Hoppscotch 合以隨時隨地產生 API 文件。"
"generate_message": "匯入 Hoppscotch 合以隨時隨地產生 API 文件。"
},
"empty": {
"authorization": "該請求沒有使用任何授權",
"body": "該請求沒有任何請求主體",
"collection": "合為空",
"collections": "合為空",
"collection": "合為空",
"collections": "合為空",
"documentation": "連線到 GraphQL 端點以檢視文件",
"endpoint": "端點不能留空",
"environments": "環境為空",
@@ -209,7 +209,7 @@
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
"check_console_details": "檢查控制台日誌以獲悉詳情",
"curl_invalid_format": "cURL 格式不正確",
"danger_zone": "Danger zone",
"danger_zone": "危險地帶",
"delete_account": "您的帳號目前為這些團隊的擁有者:",
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
"empty_req_name": "空請求名稱",
@@ -277,38 +277,38 @@
"tests": "編寫測試指令碼以自動除錯。"
},
"hide": {
"collection": "隱藏合面板",
"collection": "隱藏合面板",
"more": "隱藏更多",
"preview": "隱藏預覽",
"sidebar": "隱藏側邊欄"
},
"import": {
"collections": "匯入合",
"collections": "匯入合",
"curl": "匯入 cURL",
"failed": "匯入失敗",
"from_gist": "從 Gist 匯入",
"from_gist_description": "從 Gist 網址匯入",
"from_insomnia": "從 Insomnia 匯入",
"from_insomnia_description": "從 Insomnia 合匯入",
"from_insomnia_description": "從 Insomnia 合匯入",
"from_json": "從 Hoppscotch 匯入",
"from_json_description": "從 Hoppscotch 合檔匯入",
"from_my_collections": "從我的合匯入",
"from_my_collections_description": "從我的合檔匯入",
"from_json_description": "從 Hoppscotch 合檔匯入",
"from_my_collections": "從我的合匯入",
"from_my_collections_description": "從我的合檔匯入",
"from_openapi": "從 OpenAPI 匯入",
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
"from_postman": "從 Postman 匯入",
"from_postman_description": "從 Postman 合匯入",
"from_postman_description": "從 Postman 合匯入",
"from_url": "從網址匯入",
"gist_url": "輸入 Gist 網址",
"import_from_url_invalid_fetch": "無法從網址取得資料",
"import_from_url_invalid_file_format": "匯入合時發生錯誤",
"import_from_url_invalid_file_format": "匯入合時發生錯誤",
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
"import_from_url_success": "已匯入合",
"json_description": "從 Hoppscotch 合 JSON 檔匯入合",
"import_from_url_success": "已匯入合",
"json_description": "從 Hoppscotch 合 JSON 檔匯入合",
"title": "匯入"
},
"layout": {
"collapse_collection": "隱藏或顯示合",
"collapse_collection": "隱藏或顯示合",
"collapse_sidebar": "隱藏或顯示側邊欄",
"column": "垂直版面",
"name": "配置",
@@ -316,8 +316,8 @@
"zen_mode": "專注模式"
},
"modal": {
"close_unsaved_tab": "You have unsaved changes",
"collections": "合",
"close_unsaved_tab": "您有未儲存的改動",
"collections": "合",
"confirm": "確認",
"edit_request": "編輯請求",
"import_export": "匯入/匯出"
@@ -374,9 +374,9 @@
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
"no_permission": "您沒有權限執行此操作。",
"owner": "擁有者",
"owner_description": "擁有者可以新增、編輯和刪除請求、合和團隊成員。",
"owner_description": "擁有者可以新增、編輯和刪除請求、合和團隊成員。",
"roles": "角色",
"roles_description": "角色用來控制對共用合的存取權。",
"roles_description": "角色用來控制對共用合的存取權。",
"updated": "已更新個人檔案",
"viewer": "檢視者",
"viewer_description": "檢視者只能檢視和使用請求。"
@@ -396,8 +396,8 @@
"text": "文字"
},
"copy_link": "複製連結",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"different_collection": "無法重新排列來自不同集合的請求",
"duplicated": "已複製請求",
"duration": "持續時間",
"enter_curl": "輸入 cURL",
"generate_code": "產生程式碼",
@@ -405,10 +405,10 @@
"header_list": "請求標頭列表",
"invalid_name": "請提供請求名稱",
"method": "方法",
"moved": "Request moved",
"moved": "已移動請求",
"name": "請求名稱",
"new": "新請求",
"order_changed": "Request Order Updated",
"order_changed": "已更新請求順序",
"override": "覆寫",
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
"overriden": "已覆寫",
@@ -432,7 +432,7 @@
"view_my_links": "檢視我的連結"
},
"response": {
"audio": "Audio",
"audio": "音訊",
"body": "回應本體",
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
"headers": "回應標頭",
@@ -446,7 +446,7 @@
"status": "狀態",
"time": "時間",
"title": "回應",
"video": "Video",
"video": "視訊",
"waiting_for_connection": "等待連線",
"xml": "XML"
},
@@ -494,7 +494,7 @@
"short_codes_description": "我們為您打造的快捷碼。",
"sidebar_on_left": "左側邊欄",
"sync": "同步",
"sync_collections": "合",
"sync_collections": "合",
"sync_description": "這些設定會同步到雲端。",
"sync_environments": "環境",
"sync_history": "歷史",
@@ -551,7 +551,7 @@
"previous_method": "選擇上一個方法",
"put_method": "選擇 PUT 方法",
"reset_request": "重置請求",
"save_to_collections": "儲存到合",
"save_to_collections": "儲存到合",
"send_request": "傳送請求",
"title": "請求"
},
@@ -570,7 +570,7 @@
},
"show": {
"code": "顯示程式碼",
"collection": "顯示合面板",
"collection": "顯示合面板",
"more": "顯示更多",
"sidebar": "顯示側邊欄"
},
@@ -639,9 +639,9 @@
"tab": {
"authorization": "授權",
"body": "請求本體",
"collections": "合",
"collections": "合",
"documentation": "幫助文件",
"environments": "Environments",
"environments": "環境",
"headers": "請求標頭",
"history": "歷史記錄",
"mqtt": "MQTT",
@@ -666,7 +666,7 @@
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
"exit": "退出團隊",
"exit_disabled": "團隊擁有者無法退出團隊",
"invalid_coll_id": "Invalid collection ID",
"invalid_coll_id": "集合 ID 無效",
"invalid_email_format": "電子信箱格式無效",
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
"invalid_invite_link": "邀請連結無效",
@@ -690,21 +690,21 @@
"member_removed": "使用者已移除",
"member_role_updated": "使用者角色已更新",
"members": "成員",
"more_members": "+{count} more",
"more_members": "還有 {count} ",
"name_length_insufficient": "團隊名稱至少為 6 個字元",
"name_updated": "團隊名稱已更新",
"new": "新團隊",
"new_created": "已建立新團隊",
"new_name": "我的新團隊",
"no_access": "您沒有編輯合的許可權",
"no_access": "您沒有編輯合的許可權",
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
"no_request_found": "Request not found.",
"no_request_found": "找不到請求。",
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
"parent_coll_move": "Cannot move collection to a child collection",
"parent_coll_move": "無法將集合移動至子集合",
"pending_invites": "待定邀請",
"permissions": "許可權",
"same_target_destination": "Same target and destination",
"same_target_destination": "目標和目的地相同",
"saved": "團隊已儲存",
"select_a_team": "選擇團隊",
"title": "團隊",
@@ -734,9 +734,9 @@
"url": "網址"
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
"change": "切換工作區",
"personal": "我的工作區",
"team": "團隊工作區",
"title": "工作區"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.4.7",
"version": "2023.4.8",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -89,7 +89,6 @@
"util": "^0.12.4",
"uuid": "^8.3.2",
"vue": "^3.2.25",
"vue-github-button": "^3.0.3",
"vue-i18n": "^9.2.2",
"vue-pdf-embed": "^1.1.4",
"vue-router": "^4.0.16",
@@ -111,7 +110,7 @@
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
"@graphql-codegen/urql-introspection": "^2.2.0",
"@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",
"@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.1.4",
@@ -148,7 +147,7 @@
"vite-plugin-html-config": "^1.0.10",
"vite-plugin-inspect": "^0.7.4",
"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-vue-layouts": "^0.7.0",
"vite-plugin-windicss": "^1.8.8",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#6366f1" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#fff" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>

After

Width:  |  Height:  |  Size: 389 B

View File

@@ -9,6 +9,7 @@ declare module '@vue/runtime-core' {
export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.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']
AppFooter: typeof import('./components/app/Footer.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']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.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']
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
@@ -75,27 +77,6 @@ declare module '@vue/runtime-core' {
History: typeof import('./components/history/index.vue')['default']
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
@@ -132,6 +113,7 @@ declare module '@vue/runtime-core' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']

View File

@@ -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>

View File

@@ -15,18 +15,21 @@
:label="t('app.name')"
to="/"
/>
<!-- <AppGitHubStarButton class="mt-1.5 transition" /> -->
</div>
<div class="inline-flex items-center space-x-2">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${t(
'app.search'
)} <kbd>${getPlatformSpecialKey()}</kbd> <kbd>K</kbd>`"
:icon="IconSearch"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
<div class="inline-flex items-center justify-center flex-1 space-x-2">
<button
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"
@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
v-if="showInstallButton"
v-tippy="{ theme: 'tooltip' }"
@@ -44,6 +47,8 @@
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="invokeAction('modals.support.toggle')"
/>
</div>
<div class="inline-flex items-center justify-end flex-1 space-x-2">
<div
v-if="currentUser === null"
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 IconUserPlus from "~icons/lucide/user-plus"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import IconSearch from "~icons/lucide/search"
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
import { platform } from "~/platform"

View File

@@ -15,7 +15,10 @@
</div>
</div>
<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" />
</HoppSmartPlaceholder>
<details

View File

@@ -22,10 +22,11 @@
</div>
<div class="flex">
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
<kbd class="shortcut-key">K</kbd>
<kbd class="shortcut-key">/</kbd>
</div>
<div class="flex">
<kbd class="shortcut-key">/</kbd>
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
<kbd class="shortcut-key">K</kbd>
</div>
<div class="flex">
<kbd class="shortcut-key">?</kbd>

View File

@@ -193,7 +193,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconFolder from "~icons/lucide/folder"
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 { useI18n } from "@composables/i18n"
import { TippyComponent } from "vue-tippy"
@@ -209,67 +209,36 @@ type FolderType = "collection" | "folder"
const t = useI18n()
const props = defineProps({
id: {
type: String,
default: "",
required: true,
},
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.
* folderType is used to determine which one it is.
*/
folderType: {
type: String as PropType<FolderType>,
default: "collection",
required: true,
},
isOpen: {
type: Boolean,
default: false,
required: true,
},
isSelected: {
type: Boolean as PropType<boolean | null>,
default: false,
required: false,
},
exportLoading: {
type: Boolean,
default: false,
required: 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 props = withDefaults(
defineProps<{
id: string
parentID?: string | null
data: HoppCollection<HoppRESTRequest> | TeamCollection
/**
* Collection component can be used for both collections and folders.
* folderType is used to determine which one it is.
*/
collectionsType: CollectionType
folderType: FolderType
isOpen: boolean
isSelected?: boolean | null
exportLoading?: boolean
hasNoTeamAccess?: boolean
collectionMoveLoading?: string[]
isLastItem?: boolean
}>(),
{
id: "",
parentID: null,
collectionsType: "my-collections",
folderType: "collection",
isOpen: false,
isSelected: false,
exportLoading: false,
hasNoTeamAccess: false,
isLastItem: false,
}
)
const emit = defineEmits<{
(event: "toggle-children"): void
@@ -448,8 +417,13 @@ const notSameDestination = computed(() => {
})
const isCollLoading = computed(() => {
if (props.collectionMoveLoading.length > 0 && props.data.id) {
return props.collectionMoveLoading.includes(props.data.id)
const { collectionMoveLoading } = props
if (
collectionMoveLoading &&
collectionMoveLoading.length > 0 &&
props.data.id
) {
return collectionMoveLoading.includes(props.data.id)
} else {
return false
}

View 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>

View File

@@ -8,7 +8,7 @@
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="bg-transparent border-b border-dividerLight select-wrapper"
class="bg-transparent select-wrapper"
>
<HoppButtonSecondary
:icon="IconLayers"
@@ -22,6 +22,7 @@
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
@@ -31,6 +32,7 @@
@keyup.escape="hide()"
>
<HoppSmartItem
v-if="!isScopeSelector"
:label="`${t('environment.no_environment')}`"
:info-icon="
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
v-model="selectedEnvTab"
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}`"
:icon="IconLayers"
:label="gen.name"
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
:active-info-icon="index === selectedEnv.index"
:info-icon="isEnvActive(index) ? IconCheck : undefined"
:active-info-icon="isEnvActive(index)"
@click="
() => {
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
handleEnvironmentChange(index, {
type: 'my-environment',
environment: gen,
})
hide()
}
"
@@ -96,18 +116,14 @@
:key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name"
:info-icon="
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
:active-info-icon="isEnvActive(gen.id)"
@click="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
handleEnvironmentChange(index, {
type: 'team-environment',
environment: gen,
})
hide()
}
"
@@ -136,9 +152,10 @@
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue"
import { computed, onMounted, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
import IconGlobe from "~icons/lucide/globe"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient"
@@ -156,6 +173,31 @@ import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
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 mdAndLarger = breakpoints.greater("md")
@@ -170,6 +212,39 @@ const myEnvironments = useReadonlyStream(environments$, [])
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 teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
@@ -204,63 +279,152 @@ watch(
}
)
// 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)
const handleEnvironmentChange = (
index: number,
env?:
| {
type: "my-environment"
environment: Environment
}
| {
type: "team-environment"
environment: TeamEnvironment
}
) => {
if (props.isScopeSelector && env) {
if (env.type === "my-environment") {
emit("update:modelValue", {
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,
}
}
}
)
}
const isEnvActive = (id: string | number) => {
if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") {
return props.modelValue.index === id
} else if (props.modelValue?.type === "team-environment") {
return (
props.modelValue?.type === "team-environment" &&
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(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
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: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
name: props.modelValue.environment.environment.name,
teamEnvID: props.modelValue.environment.id,
}
} else {
return { type: "global", name: "Global" }
}
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
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",
})
}
}
})

View File

@@ -26,6 +26,13 @@
:editing-variable-name="editingVariableName"
@hide-modal="displayModalEdit(false)"
/>
<EnvironmentsAdd
:show="showModalNew"
:name="editingVariableName"
:value="editingVariableValue"
:position="position"
@hide-modal="displayModalNew(false)"
/>
</div>
</template>
@@ -161,10 +168,18 @@ watch(
}
)
const showModalNew = ref(false)
const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit")
const editingEnvironmentIndex = ref<"Global" | null>(null)
const editingVariableName = ref("")
const editingVariableValue = ref("")
const position = ref({ top: 0, left: 0 })
const displayModalNew = (shouldDisplay: boolean) => {
showModalNew.value = shouldDisplay
}
const displayModalEdit = (shouldDisplay: boolean) => {
action.value = "edit"
@@ -233,4 +248,10 @@ watch(
},
{ deep: true }
)
defineActionHandler("modals.environment.add", ({ envName, variableName }) => {
editingVariableName.value = envName
editingVariableValue.value = variableName
displayModalNew(true)
})
</script>

View File

@@ -61,7 +61,8 @@ import { useReadonlyStream } from "@composables/stream"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { platform } from "~/platform"
import { useI18n } from "~/composables/i18n"
import { onClickOutside } from "@vueuse/core"
import { onClickOutside, useDebounceFn } from "@vueuse/core"
import { invokeAction } from "~/helpers/actions"
const props = withDefaults(
defineProps<{
@@ -149,6 +150,11 @@ const handleKeystroke = (ev: KeyboardEvent) => {
ev.preventDefault()
}
if (ev.shiftKey) {
showSuggestionPopover.value = false
return
}
showSuggestionPopover.value = true
if (
@@ -299,8 +305,46 @@ const envVars = computed(() =>
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
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 = [
EditorView.lineWrapping,
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
EditorView.updateListener.of((update) => {
if (props.readonly) {
update.view.contentDOM.inputMode = "none"
@@ -431,7 +475,7 @@ watch(editor, () => {
@apply border-b border-x border-divider;
@apply overflow-y-auto;
@apply -left-[1px];
@apply right-0;
@apply -right-[1px];
top: calc(100% + 1px);
border-radius: 0 0 8px 8px;

View File

@@ -10,6 +10,7 @@
class="flex flex-col flex-1"
>
<SmartTreeBranch
:root-nodes-length="rootNodes.data.length"
:node-item="rootNode"
:adapter="adapter as SmartTreeAdapter<T>"
>

View File

@@ -85,19 +85,25 @@ const props = defineProps<{
* The node item that will be used to render the tree branch content
*/
nodeItem: TreeNode<T>
/**
* Total number of rootNode
*/
rootNodesLength?: number
}>()
const CHILD_SLOT_NAME = "default"
const t = useI18n()
const isOnlyRootChild = computed(() => props.rootNodesLength === 1)
/**
* Marks whether the children on this branch were ever rendered
* See the usage inside '<template>' for more info
*/
const childrenRendered = ref(false)
const childrenRendered = ref(isOnlyRootChild.value)
const showChildren = ref(false)
const isNodeOpen = ref(false)
const showChildren = ref(isOnlyRootChild.value)
const isNodeOpen = ref(isOnlyRootChild.value)
const highlightNode = ref(false)

View File

@@ -40,6 +40,8 @@ import {
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment"
import xmlFormat from "xml-formatter"
import { platform } from "~/platform"
import { invokeAction } from "~/helpers/actions"
import { useDebounceFn } from "@vueuse/core"
// TODO: Migrate from legacy mode
type ExtendedEditorConfig = {
@@ -218,6 +220,40 @@ export function useCodemirror(
ViewPlugin.fromClass(
class {
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 line = update.state.doc.lineAt(cursorPos)
@@ -276,6 +312,7 @@ export function useCodemirror(
run: indentLess,
},
]),
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
]
if (environmentTooltip) extensions.push(environmentTooltip.extension)

View File

@@ -8,6 +8,7 @@ import { HoppRESTDocument } from "./rest/document"
import { HoppGQLRequest } from "@hoppscotch/data"
export type HoppAction =
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
| "request.reset" // Clear request data
| "request.copy-link" // Copy Request Link
@@ -24,6 +25,9 @@ export type HoppAction =
| "modals.search.toggle" // Shows the search modal
| "modals.support.toggle" // Shows the support 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.graphql" // Jump to GraphQL 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
*/
type HoppActionArgsMap = {
"contextmenu.open": {
position: {
top: number
left: number
}
text: string | null
}
"modals.my.environment.edit": {
envName: string
variableName: string
@@ -68,6 +79,10 @@ type HoppActionArgsMap = {
"gql.request.open": {
request: HoppGQLRequest
}
"modals.environment.add": {
envName: string
variableName: string
}
}
/**

View File

@@ -14,7 +14,13 @@ let keybindingsEnabled = true
* Alt is also regarded as macOS OPTION (⌥) key
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
*/
type ModifierKeys = "ctrl" | "alt" | "ctrl-shift" | "alt-shift"
type ModifierKeys =
| "ctrl"
| "alt"
| "ctrl-shift"
| "alt-shift"
| "ctrl-alt"
| "ctrl-alt-shift"
/* eslint-disable prettier/prettier */
// prettier-ignore
@@ -143,18 +149,19 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
}
function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
const isShiftKey = ev.shiftKey
const modifierKeys = {
ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
alt: ev.altKey,
shift: ev.shiftKey,
}
// We only allow one modifier key to be pressed (for now)
// Control key (+ Command) gets priority and if Alt is also pressed, it is ignored
if (isAppleDevice() && ev.metaKey) return isShiftKey ? "ctrl-shift" : "ctrl"
else if (!isAppleDevice() && ev.ctrlKey)
return isShiftKey ? "ctrl-shift" : "ctrl"
// active modifier: ctrl | alt | ctrl-alt | ctrl-shift | ctrl-alt-shift | alt-shift
// modiferKeys object's keys are sorted to match the above order
const activeModifier = Object.keys(modifierKeys)
.filter((key) => modifierKeys[key as keyof typeof modifierKeys])
.join("-")
// Test for Alt key
if (ev.altKey) return isShiftKey ? "alt-shift" : "alt"
return null
return activeModifier === "" ? null : (activeModifier as ModifierKeys)
}
/**

View File

@@ -16,12 +16,12 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
},
{
label: t("shortcut.general.command_menu"),
keys: ["/"],
keys: [getPlatformSpecialKey(), "K"],
section: t("shortcut.general.title"),
},
{
label: t("shortcut.general.show_all"),
keys: [getPlatformSpecialKey(), "K"],
keys: [getPlatformSpecialKey(), "/"],
section: t("shortcut.general.title"),
},
{

View File

@@ -84,6 +84,13 @@
:show="savingRequest"
@hide-modal="onSaveModalClose"
/>
<AppContextMenu
v-if="contextMenu.show"
:show="contextMenu.show"
:position="contextMenu.position"
:text="contextMenu.text"
@hide-modal="contextMenu.show = false"
/>
</div>
</template>
@@ -138,6 +145,24 @@ const reqName = ref<string>("")
const t = useI18n()
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 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()
bindRequestToURLParams()
oAuthURL()

View File

@@ -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([])
})
})
})

View 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
}
}

View File

@@ -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,
}
)
})
})
})

View File

@@ -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)
})
})
})
})

View File

@@ -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,
})
})
})
})

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -6,7 +6,7 @@ WORKDIR /usr/src/app
RUN npm i -g pnpm
COPY . .
RUN pnpm install
RUN pnpm install --force --frozen-lockfile
WORKDIR /usr/src/app/packages/hoppscotch-selfhost-web/
RUN pnpm run build

View File

@@ -1,13 +1,13 @@
{
"name": "@hoppscotch/selfhost-web",
"private": true,
"version": "2023.4.7",
"version": "2023.4.8",
"type": "module",
"scripts": {
"dev:vite": "vite",
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\" --watch",
"dev": "pnpm exec npm-run-all -p -l dev:*",
"build": "node --max_old_space_size=16384 ./node_modules/vite/bin/vite.js build",
"build": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build",
"preview": "vite preview",
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
"lint:ts": "vue-tsc --noEmit",
@@ -66,7 +66,7 @@
"vite-plugin-html-config": "^1.0.10",
"vite-plugin-inspect": "^0.7.4",
"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-static-copy": "^0.12.0",
"vite-plugin-vue-layouts": "^0.7.0",

View File

@@ -78,16 +78,14 @@ export default defineConfig({
routeStyle: "nuxt",
dirs: "../hoppscotch-common/src/pages",
importMode: "async",
onRoutesGenerated(routes) {
// HACK: See: https://github.com/jbaubree/vite-plugin-pages-sitemap/issues/173
return ((generateSitemap as any).default as typeof generateSitemap)({
onRoutesGenerated: (routes) =>
generateSitemap({
routes,
nuxtStyle: true,
allowRobots: true,
dest: ".sitemap-gen",
hostname: ENV.VITE_BASE_URL,
})
},
}),
}),
StaticCopy({
targets: [

View File

@@ -6,7 +6,7 @@ WORKDIR /usr/src/app
RUN npm i -g pnpm
COPY . .
RUN pnpm install
RUN pnpm install --force --frozen-lockfile
WORKDIR /usr/src/app/packages/hoppscotch-sh-admin/
RUN pnpm run build

View File

@@ -4,6 +4,7 @@
@apply after:backface-hidden;
@apply selection:bg-accentDark;
@apply selection:text-accentContrast;
@apply overscroll-none;
}
:root {

View File

@@ -1,7 +1,7 @@
{
"name": "hoppscotch-sh-admin",
"private": true,
"version": "2023.4.7",
"version": "2023.4.8",
"type": "module",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",

View File

@@ -47,7 +47,7 @@
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@esbuild-plugins/node-modules-polyfill": "^0.1.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",
"@rushstack/eslint-patch": "^1.1.4",
"@types/lodash-es": "^4.17.6",
@@ -71,12 +71,12 @@
"unplugin-vue-components": "^0.21.0",
"vite": "^3.2.3",
"vite-plugin-checker": "^0.5.1",
"vite-plugin-dts": "2.0.0-beta.3",
"vite-plugin-dts": "3.2.0",
"vite-plugin-fonts": "^0.6.0",
"vite-plugin-html-config": "^1.0.10",
"vite-plugin-inspect": "^0.7.4",
"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-vue-layouts": "^0.7.0",
"vite-plugin-windicss": "^1.8.8",

View File

@@ -4,6 +4,7 @@
@apply after:backface-hidden;
@apply selection:bg-accentDark;
@apply selection:text-accentContrast;
@apply overscroll-none;
}
:root {

View File

@@ -1,62 +1,55 @@
<template>
<HoppSmartLink :to="to" :exact="exact" :blank="blank" class="inline-flex items-center justify-center focus:outline-none"
<HoppSmartLink
:to="to"
:exact="exact"
:blank="blank"
class="inline-flex items-center justify-center focus:outline-none"
:class="[
color
? `text-${color}-500 hover:text-${color}-600 focus-visible:text-${color}-600`
: 'hover:text-secondaryDark focus-visible:text-secondaryDark',
{ 'opacity-75 cursor-not-allowed': disabled },
{ 'flex-row-reverse': reverse },
]" :disabled="disabled" tabindex="0">
<component :is="icon" v-if="icon" class="svg-icons" :class="label ? (reverse ? 'ml-2' : 'mr-2') : ''" />
]"
:disabled="disabled"
tabindex="0"
>
<component
:is="icon"
v-if="icon"
class="svg-icons"
:class="label ? (reverse ? 'ml-2' : 'mr-2') : ''"
/>
{{ label }}
</HoppSmartLink>
</template>
<script lang="ts">
import HoppSmartLink from "./Link.vue";
import { Component, defineComponent, PropType } from "vue"
<script setup lang="ts">
import HoppSmartLink from "./Link.vue"
import type { Component } from "vue"
export default defineComponent({
components: {
HoppSmartLink
},
props: {
to: {
type: String,
default: "",
},
exact: {
type: Boolean,
default: true,
},
blank: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
icon: {
type: Object as PropType<Component | null>,
default: null,
},
svg: {
type: Object as PropType<Component | null>,
default: null,
},
color: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
},
reverse: {
type: Boolean,
default: false,
},
},
})
withDefaults(
defineProps<{
to: string
exact: boolean
blank: boolean
label: string
icon: Component | null
svg: Component | null
color: string
disabled: boolean
reverse: boolean
}>(),
{
to: "",
exact: true,
blank: false,
label: "",
icon: null,
svg: null,
color: "",
disabled: false,
reverse: false,
}
)
</script>

View File

@@ -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"
>
<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"
>
<div
@@ -455,6 +455,7 @@ $slider-height: 4px;
@apply min-w-0;
@apply bg-dividerDark;
@apply hover:bg-secondaryLight;
@apply active:bg-secondaryLight;
width: var(--thumb-width);
height: $slider-height;
@@ -465,6 +466,7 @@ $slider-height: 4px;
@apply min-w-0;
@apply bg-dividerDark;
@apply hover:bg-secondaryLight;
@apply active:bg-secondaryLight;
width: var(--thumb-width);
height: $slider-height;

View File

@@ -11,14 +11,13 @@ export default defineConfig({
vue(),
dts({
insertTypesEntry: true,
skipDiagnostics: true,
outputDir: ['dist']
outDir: ["dist"],
}),
WindiCSS({
root: path.resolve(__dirname),
}),
Icons({
compiler: "vue3"
compiler: "vue3",
}),
VitePluginFonts({
google: {
@@ -45,6 +44,6 @@ export default defineConfig({
exports: "named",
},
},
emptyOutDir: true
emptyOutDir: true,
},
})

263
pnpm-lock.yaml generated
View File

@@ -575,9 +575,6 @@ importers:
vue:
specifier: ^3.2.25
version: 3.2.37
vue-github-button:
specifier: ^3.0.3
version: 3.0.3
vue-i18n:
specifier: ^9.2.2
version: 9.2.2(vue@3.2.37)
@@ -637,8 +634,8 @@ importers:
specifier: ^3.1.1
version: 3.1.1(graphql@15.8.0)
'@iconify-json/lucide':
specifier: ^1.1.40
version: 1.1.40
specifier: ^1.1.109
version: 1.1.109
'@intlify/vite-plugin-vue-i18n':
specifier: ^7.0.0
version: 7.0.0(vite@3.1.4)(vue-i18n@9.2.2)
@@ -748,8 +745,8 @@ importers:
specifier: ^0.26.0
version: 0.26.0(@vue/compiler-sfc@3.2.39)(vite@3.1.4)
vite-plugin-pages-sitemap:
specifier: ^1.4.0
version: 1.4.0
specifier: ^1.4.5
version: 1.4.5
vite-plugin-pwa:
specifier: ^0.13.1
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
version: 0.26.0(@vue/compiler-sfc@3.2.45)(vite@3.2.4)
vite-plugin-pages-sitemap:
specifier: ^1.4.0
version: 1.4.0
specifier: ^1.4.5
version: 1.4.5
vite-plugin-pwa:
specifier: ^0.13.1
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
version: 0.12.4(histoire@0.12.4)(vite@3.2.4)(vue@3.2.45)
'@iconify-json/lucide':
specifier: ^1.1.40
version: 1.1.40
specifier: ^1.1.109
version: 1.1.109
'@intlify/vite-plugin-vue-i18n':
specifier: ^6.0.1
version: 6.0.1(vite@3.2.4)
@@ -1311,8 +1308,8 @@ importers:
specifier: ^0.5.1
version: 0.5.1(eslint@8.29.0)(typescript@4.9.3)(vite@3.2.4)
vite-plugin-dts:
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3(@types/node@17.0.45)(rollup@2.79.1)(vite@3.2.4)
specifier: 3.2.0
version: 3.2.0(@types/node@17.0.45)(sass@1.53.0)(terser@5.14.1)(typescript@4.9.3)
vite-plugin-fonts:
specifier: ^0.6.0
version: 0.6.0(vite@3.2.4)
@@ -1326,8 +1323,8 @@ importers:
specifier: ^0.26.0
version: 0.26.0(@vue/compiler-sfc@3.2.45)(vite@3.2.4)
vite-plugin-pages-sitemap:
specifier: ^1.4.0
version: 1.4.0
specifier: ^1.4.5
version: 1.4.5
vite-plugin-pwa:
specifier: ^0.13.1
version: 0.13.1(vite@3.2.4)(workbox-build@6.5.4)(workbox-window@6.5.4)
@@ -2014,14 +2011,6 @@ packages:
dependencies:
'@babel/types': 7.18.7
/@babel/parser@7.20.15:
resolution: {integrity: sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.20.7
dev: true
/@babel/parser@7.22.5:
resolution: {integrity: sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==}
engines: {node: '>=6.0.0'}
@@ -2942,7 +2931,7 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.18.6
'@babel/parser': 7.20.15
'@babel/parser': 7.22.5
'@babel/types': 7.20.7
dev: true
@@ -5786,10 +5775,10 @@ packages:
/@iarna/toml@2.2.5:
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
/@iconify-json/lucide@1.1.40:
resolution: {integrity: sha512-4GeQtaiv3mJ+b0sn/c2KL8Tgf4XQvsX1AHDOseuGRhgoLCWG+ZdNRFxF5sp1I6T/VcQccegLPOp5XHn3NC1mmA==}
/@iconify-json/lucide@1.1.109:
resolution: {integrity: sha512-1+zYieiKUAjN1x66kvcRmmtgBJaDbD7i4To8mhB6+3bEm/i61un76nspJ45LOSGovzBMvYZFIJpqJrGMipWPzw==}
dependencies:
'@iconify/types': 1.1.0
'@iconify/types': 2.0.0
dev: true
/@iconify/types@1.1.0:
@@ -5836,8 +5825,8 @@ packages:
vue-i18n:
optional: true
dependencies:
'@intlify/message-compiler': 9.3.0-beta.24
'@intlify/shared': 9.3.0-beta.24
'@intlify/message-compiler': 9.3.0-beta.25
'@intlify/shared': 9.3.0-beta.25
jsonc-eslint-parser: 1.4.1
source-map: 0.6.1
vue-i18n: 9.2.2(vue@3.2.37)
@@ -5898,11 +5887,11 @@ packages:
source-map-js: 1.0.2
dev: true
/@intlify/message-compiler@9.3.0-beta.24:
resolution: {integrity: sha512-prhHATkgp0mpPqoVgiAtLmUc1JMvs8fMH6w53AVEBn+VF87dLhzanfmWY5FoZWORG51ag54gBDBOoM/VFv3m3A==}
/@intlify/message-compiler@9.3.0-beta.25:
resolution: {integrity: sha512-uT7ybqKoDEw1XITQYnTYjWgZnpCDmHv9e3D4MmJDqHl2qCm6anzdUXWKHUhqR87Ha9Z8Rl44v40iSI/4NUbppQ==}
engines: {node: '>= 16'}
dependencies:
'@intlify/shared': 9.3.0-beta.24
'@intlify/shared': 9.3.0-beta.25
source-map-js: 1.0.2
dev: true
@@ -5915,8 +5904,8 @@ packages:
engines: {node: '>= 16'}
dev: true
/@intlify/shared@9.3.0-beta.24:
resolution: {integrity: sha512-AKxJ8s7eKIQWkNaf4wyyoLRwf4puCuQgjSChlDJm5JBEt6T8HGgnYTJLRXu6LD/JACn3Qwu6hM/XRX1c9yvjmQ==}
/@intlify/shared@9.3.0-beta.25:
resolution: {integrity: sha512-Zg+ECV9RPdp227tCJOgvPb+S3i651nf4kKHsMojSyWCppVK/4NFuDrBG2lIQSQL6Iq5LKVr5MkezHCW2NBTQRg==}
engines: {node: '>= 16'}
dev: true
@@ -5936,7 +5925,7 @@ packages:
optional: true
dependencies:
'@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
debug: 4.3.4(supports-color@9.2.2)
fast-glob: 3.2.11
@@ -5963,7 +5952,7 @@ packages:
optional: true
dependencies:
'@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
debug: 4.3.4(supports-color@9.2.2)
fast-glob: 3.2.12
@@ -5991,7 +5980,7 @@ packages:
optional: true
dependencies:
'@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
debug: 4.3.4(supports-color@9.2.2)
fast-glob: 3.2.12
@@ -6623,32 +6612,32 @@ packages:
resolution: {integrity: sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA==}
dev: true
/@microsoft/api-extractor-model@7.26.4(@types/node@17.0.45):
resolution: {integrity: sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==}
/@microsoft/api-extractor-model@7.27.4(@types/node@17.0.45):
resolution: {integrity: sha512-HjqQFmuGPOS20rtnu+9Jj0QrqZyR59E+piUWXPMZTTn4jaZI+4UmsHSf3Id8vyueAhOBH2cgwBuRTE5R+MfSMw==}
dependencies:
'@microsoft/tsdoc': 0.14.2
'@microsoft/tsdoc-config': 0.16.2
'@rushstack/node-core-library': 3.55.2(@types/node@17.0.45)
'@rushstack/node-core-library': 3.59.5(@types/node@17.0.45)
transitivePeerDependencies:
- '@types/node'
dev: true
/@microsoft/api-extractor@7.34.4(@types/node@17.0.45):
resolution: {integrity: sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==}
/@microsoft/api-extractor@7.36.1(@types/node@17.0.45):
resolution: {integrity: sha512-2SPp1jq6wDY5IOsRLUv/4FxngslctBZJlztAJ3uWpCAwqKQG7ESdL3DhEza+StbYLtBQmu1Pk6q1Vkhl7qD/bg==}
hasBin: true
dependencies:
'@microsoft/api-extractor-model': 7.26.4(@types/node@17.0.45)
'@microsoft/api-extractor-model': 7.27.4(@types/node@17.0.45)
'@microsoft/tsdoc': 0.14.2
'@microsoft/tsdoc-config': 0.16.2
'@rushstack/node-core-library': 3.55.2(@types/node@17.0.45)
'@rushstack/rig-package': 0.3.18
'@rushstack/ts-command-line': 4.13.2
'@rushstack/node-core-library': 3.59.5(@types/node@17.0.45)
'@rushstack/rig-package': 0.4.0
'@rushstack/ts-command-line': 4.15.1
colors: 1.2.5
lodash: 4.17.21
resolve: 1.22.1
semver: 7.3.8
source-map: 0.6.1
typescript: 4.8.4
typescript: 5.0.4
transitivePeerDependencies:
- '@types/node'
dev: true
@@ -7228,8 +7217,8 @@ packages:
resolution: {integrity: sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==}
dev: true
/@rushstack/node-core-library@3.55.2(@types/node@17.0.45):
resolution: {integrity: sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==}
/@rushstack/node-core-library@3.59.5(@types/node@17.0.45):
resolution: {integrity: sha512-1IpV7LufrI1EoVO8hYsb3t6L8L+yp40Sa0OaOV2CIu1zx4e6ZeVNaVIEXFgMXBKdGXkAh21MnCaIzlDNpG6ZQw==}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
@@ -7246,15 +7235,15 @@ packages:
z-schema: 5.0.5
dev: true
/@rushstack/rig-package@0.3.18:
resolution: {integrity: sha512-SGEwNTwNq9bI3pkdd01yCaH+gAsHqs0uxfGvtw9b0LJXH52qooWXnrFTRRLG1aL9pf+M2CARdrA9HLHJys3jiQ==}
/@rushstack/rig-package@0.4.0:
resolution: {integrity: sha512-FnM1TQLJYwSiurP6aYSnansprK5l8WUK8VG38CmAaZs29ZeL1msjK0AP1VS4ejD33G0kE/2cpsPsS9jDenBMxw==}
dependencies:
resolve: 1.22.1
strip-json-comments: 3.1.1
dev: true
/@rushstack/ts-command-line@4.13.2:
resolution: {integrity: sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==}
/@rushstack/ts-command-line@4.15.1:
resolution: {integrity: sha512-EL4jxZe5fhb1uVL/P/wQO+Z8Rc8FMiWJ1G7VgnPDvdIt5GVjRfK7vwzder1CZQiX3x0PY6uxENYLNGTFd1InRQ==}
dependencies:
'@types/argparse': 1.0.38
argparse: 1.0.10
@@ -7516,15 +7505,6 @@ packages:
engines: {node: '>= 10'}
dev: true
/@ts-morph/common@0.18.1:
resolution: {integrity: sha512-RVE+zSRICWRsfrkAw5qCAK+4ZH9kwEFv5h0+/YeHTLieWP7F4wWq4JsKFuNWG+fYh/KF+8rAtgdj5zb2mm+DVA==}
dependencies:
fast-glob: 3.2.12
minimatch: 5.1.0
mkdirp: 1.0.4
path-browserify: 1.0.1
dev: true
/@tsconfig/node10@1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
@@ -8736,6 +8716,12 @@ packages:
muggle-string: 0.1.0
dev: true
/@volar/language-core@1.8.0:
resolution: {integrity: sha512-ZHTvZPM3pEbOOuaq+ybNz5TQlHUqPQPK0G1+SonvApGq0e3qgGijjhtL5T7hsCtUEmxfix8FrAuCH14tMBOhTg==}
dependencies:
'@volar/source-map': 1.8.0
dev: true
/@volar/shared@0.27.24:
resolution: {integrity: sha512-Mi8a4GQaiorfb+o4EqOXDZm9E/uBJXgScFgF+NhtcMBOUKHNMKQyLI7YRGumtyJTTdaX7nSDJjGGTkv23tcOtQ==}
dependencies:
@@ -8760,6 +8746,12 @@ packages:
muggle-string: 0.1.0
dev: true
/@volar/source-map@1.8.0:
resolution: {integrity: sha512-d35aV0yFkIrkynRSKgrN5hgbMv6ekkFvcJsJGmOZ8UEjqLStto9zq7RSvpp6/PZ7/pa4Gn1f6K1qDt0bq0oUew==}
dependencies:
muggle-string: 0.3.1
dev: true
/@volar/transforms@0.27.24:
resolution: {integrity: sha512-sOHi1ZSapFlxn7yPl4MO5TXd9aWC0BVq2CgXAJ2EESb+ddh2uJbGQgLLNocX+MDh419cUuuFT2QAJpuWHhJcng==}
dependencies:
@@ -8773,6 +8765,12 @@ packages:
'@volar/language-core': 1.0.9
dev: true
/@volar/typescript@1.8.0:
resolution: {integrity: sha512-T/U1XLLhXv6tNr40Awznfc6QZWizSL99t6M0DeXtIMbnvSCqjjCVRnwlsq+DK9C1RlO3k8+i0Z8iJn7O1GGtoA==}
dependencies:
'@volar/language-core': 1.8.0
dev: true
/@volar/vue-code-gen@0.38.2:
resolution: {integrity: sha512-whLunD6phSGWBUHZKdTxeglrpzQu26ii8CRVapFdjfyMaVhQ7ESNeIAhkTVyg2ovOPc0PiDYPQEPzfWAADIWog==}
dependencies:
@@ -8994,6 +8992,25 @@ packages:
- supports-color
dev: true
/@vue/language-core@1.8.4(typescript@4.9.3):
resolution: {integrity: sha512-pnNtNcJVfkGYluW0vsVO+Y1gyX+eA0voaS7+1JOhCp5zKeCaL/PAmGYOgfvwML62neL+2H8pnhY7sffmrGpEhw==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@volar/language-core': 1.8.0
'@volar/source-map': 1.8.0
'@vue/compiler-dom': 3.3.4
'@vue/reactivity': 3.3.4
'@vue/shared': 3.3.4
minimatch: 9.0.3
muggle-string: 0.3.1
typescript: 4.9.3
vue-template-compiler: 2.7.14
dev: true
/@vue/reactivity-transform@3.2.37:
resolution: {integrity: sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==}
dependencies:
@@ -9037,6 +9054,12 @@ packages:
dependencies:
'@vue/shared': 3.2.45
/@vue/reactivity@3.3.4:
resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==}
dependencies:
'@vue/shared': 3.3.4
dev: true
/@vue/runtime-core@3.2.37:
resolution: {integrity: sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==}
dependencies:
@@ -9101,6 +9124,15 @@ packages:
resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
dev: true
/@vue/typescript@1.8.4(typescript@4.9.3):
resolution: {integrity: sha512-sioQfIY5xcmEAz+cPLvv6CtzGPtGhIdR0Za87zB8M4mPe4OSsE3MBGkXcslf+EzQgF+fm6Gr1SRMSX8r5ZmzDA==}
dependencies:
'@volar/typescript': 1.8.0
'@vue/language-core': 1.8.4(typescript@4.9.3)
transitivePeerDependencies:
- typescript
dev: true
/@vueuse/core@8.7.5(vue@3.2.45):
resolution: {integrity: sha512-tqgzeZGoZcXzoit4kOGLWJibDMLp0vdm6ZO41SSUQhkhtrPhAg6dbIEPiahhUu6sZAmSYvVrZgEr5aKD51nrLA==}
peerDependencies:
@@ -9509,7 +9541,7 @@ packages:
hasBin: true
/after@0.8.2:
resolution: {integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=}
resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==}
dev: false
/agent-base@6.0.2:
@@ -10121,7 +10153,7 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
/base64-arraybuffer@0.1.4:
resolution: {integrity: sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=}
resolution: {integrity: sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==}
engines: {node: '>= 0.6.0'}
dev: false
@@ -10672,10 +10704,6 @@ packages:
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
dev: true
/code-block-writer@11.0.3:
resolution: {integrity: sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==}
dev: true
/collect-v8-coverage@1.0.1:
resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
dev: true
@@ -10772,14 +10800,14 @@ packages:
dev: true
/component-bind@1.0.0:
resolution: {integrity: sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=}
resolution: {integrity: sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==}
dev: false
/component-emitter@1.3.0:
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
/component-inherit@0.0.3:
resolution: {integrity: sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=}
resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==}
dev: false
/concat-map@0.0.1:
@@ -14055,7 +14083,7 @@ packages:
dev: false
/has-cors@1.1.0:
resolution: {integrity: sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=}
resolution: {integrity: sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==}
dev: false
/has-flag@3.0.0:
@@ -14448,7 +14476,7 @@ packages:
engines: {node: '>=8'}
/indexof@0.0.1:
resolution: {integrity: sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=}
resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
dev: false
/inflight@1.0.6:
@@ -16384,6 +16412,10 @@ packages:
resolution: {integrity: sha512-ymToLHqL02udwVdbkowNpzjFd6UzozMtshPQKVi5k1EjKRqKqBrOnE9QbLEb0/pV76SAiIT13hdL8R6suc+f3g==}
dev: true
/kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
dev: true
/leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
dev: false
@@ -16752,13 +16784,6 @@ packages:
dependencies:
sourcemap-codec: 1.4.8
/magic-string@0.29.0:
resolution: {integrity: sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/magic-string@0.30.0:
resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==}
engines: {node: '>=12'}
@@ -17021,6 +17046,13 @@ packages:
dependencies:
brace-expansion: 2.0.1
/minimatch@9.0.3:
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
brace-expansion: 2.0.1
dev: true
/minimist-options@4.1.0:
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
engines: {node: '>= 6'}
@@ -17503,6 +17535,10 @@ packages:
resolution: {integrity: sha512-Tr1knR3d2mKvvWthlk7202rywKbiOm4rVFLsfAaSIhJ6dt9o47W4S+JMtWhd/PW9Wrdew2/S2fSvhz3E2gkfEg==}
dev: true
/muggle-string@0.3.1:
resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==}
dev: true
/multer@1.4.4-lts.1:
resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==}
engines: {node: '>= 6.0.0'}
@@ -18107,10 +18143,6 @@ packages:
utils-merge: 1.0.1
dev: false
/path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
dev: true
/path-case@3.0.4:
resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==}
dependencies:
@@ -20292,7 +20324,7 @@ packages:
dev: true
/to-array@0.1.4:
resolution: {integrity: sha1-F+bBH3PdTz10zaek/zI46a2b+JA=}
resolution: {integrity: sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==}
dev: false
/to-fast-properties@2.0.0:
@@ -20473,13 +20505,6 @@ packages:
resolution: {integrity: sha512-DEQrfv6l7IvN2jlzc/VTdZJYsWUnQNCsueYjMkC/iXoEoi5fNan6MjeDqkvhfzbmHgdz9UxDUluX3V5HdjTydQ==}
dev: true
/ts-morph@17.0.1:
resolution: {integrity: sha512-10PkHyXmrtsTvZSL+cqtJLTgFXkU43Gd0JCc0Rw6GchWbqKe0Rwgt1v3ouobTZwQzF1mGhDeAlWYBMGRV7y+3g==}
dependencies:
'@ts-morph/common': 0.18.1
code-block-writer: 11.0.3
dev: true
/ts-node-dev@2.0.0(@types/node@18.16.17)(typescript@4.9.3):
resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==}
engines: {node: '>=0.8.0'}
@@ -20818,6 +20843,12 @@ packages:
hasBin: true
dev: true
/typescript@5.0.4:
resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==}
engines: {node: '>=12.20'}
hasBin: true
dev: true
/ua-parser-js@0.7.31:
resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==}
dev: true
@@ -21372,27 +21403,31 @@ packages:
vscode-uri: 3.0.3
dev: true
/vite-plugin-dts@2.0.0-beta.3(@types/node@17.0.45)(rollup@2.79.1)(vite@3.2.4):
resolution: {integrity: sha512-QrsbTxyt0choSYXPxPfmN9XcSvxcVZk0zticxLrI5DkECs9KhDrSVGok1YP/UPkoKpfF9ThtOJcM5Rjuesxv/w==}
/vite-plugin-dts@3.2.0(@types/node@17.0.45)(sass@1.53.0)(terser@5.14.1)(typescript@4.9.3):
resolution: {integrity: sha512-s+dwJvDcb/AWgb49oVbq9JiUSIMwaVpFfV4SVIaBZmv9OZyeyDGxujaq+z4HJ4LB4hUG5c4oRAJyLfV66c763Q==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: '>=2.9.0'
typescript: '*'
dependencies:
'@babel/parser': 7.20.15
'@microsoft/api-extractor': 7.34.4(@types/node@17.0.45)
'@microsoft/api-extractor': 7.36.1(@types/node@17.0.45)
'@rollup/pluginutils': 5.0.2(rollup@2.79.1)
'@rushstack/node-core-library': 3.55.2(@types/node@17.0.45)
'@rushstack/node-core-library': 3.59.5(@types/node@17.0.45)
'@vue/language-core': 1.8.4(typescript@4.9.3)
debug: 4.3.4(supports-color@9.2.2)
fast-glob: 3.2.12
fs-extra: 10.1.0
kolorist: 1.7.0
magic-string: 0.29.0
ts-morph: 17.0.1
kolorist: 1.8.0
typescript: 4.9.3
vue-tsc: 1.8.4(typescript@4.9.3)
optionalDependencies:
rollup: 2.79.1
vite: 3.2.4(@types/node@17.0.45)(sass@1.53.0)(terser@5.14.1)
transitivePeerDependencies:
- '@types/node'
- rollup
- less
- sass
- stylus
- sugarss
- supports-color
- terser
dev: true
/vite-plugin-eslint@1.8.1(eslint@8.29.0)(vite@3.2.4):
@@ -21478,8 +21513,8 @@ packages:
- supports-color
dev: true
/vite-plugin-pages-sitemap@1.4.0:
resolution: {integrity: sha512-8GlmNSeObvDVUdmgutfWnGBZGsn3Zb8IXCjRODFB0tOhQqAQPZRwXrFha6ZLjvF4DgVJI3sW2FfiXa1wFxJvmg==}
/vite-plugin-pages-sitemap@1.4.5:
resolution: {integrity: sha512-AcEoJ+0D0P1CwR1LjzBznHs6yNQVP7ha7l6cl/VHrHFcQXbVyKc+QOmLi1a3eONy+aDA3K01pZVK8TzTIufq/w==}
dev: true
/vite-plugin-pages@0.26.0(@vue/compiler-sfc@3.2.39)(vite@3.1.4):
@@ -22307,6 +22342,18 @@ packages:
typescript: 4.9.3
dev: true
/vue-tsc@1.8.4(typescript@4.9.3):
resolution: {integrity: sha512-+hgpOhIx11vbi8/AxEdaPj3fiRwN9wy78LpsNNw2V995/IWa6TMyQxHbaw2ZKUpdwjySSHgrT6ohDEhUgFxGYw==}
hasBin: true
peerDependencies:
typescript: '*'
dependencies:
'@vue/language-core': 1.8.4(typescript@4.9.3)
'@vue/typescript': 1.8.4(typescript@4.9.3)
semver: 7.3.8
typescript: 4.9.3
dev: true
/vue@2.7.1:
resolution: {integrity: sha512-X1YkFddhbTAU2FPK0gBZ/vDOcOMA8ZT4uHoFVor1bUb7BpVGdEswS286YGtODsf/Ghfr1LM1sBMFAY8XT+dVhA==}
dependencies:
@@ -23115,7 +23162,7 @@ packages:
dev: false
/yeast@0.1.2:
resolution: {integrity: sha1-AI4G2AlDIMNy28L47XagymyKxBk=}
resolution: {integrity: sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==}
dev: false
/yn@3.1.1:
@@ -23154,3 +23201,7 @@ packages:
optionalDependencies:
commander: 9.5.0
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false