Compare commits

...

18 Commits

Author SHA1 Message Date
Mir Arif Hasan
a4fdc05b5f feat: .env.example updated 2023-08-08 14:10:50 +05:30
Mir Arif Hasan
4a0a5e6a04 feat: env variable changed 2023-08-08 14:10:50 +05:30
Nivedin
085fbb2a9b feat: tippy menu for history and tab (#3220)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-08 13:23:11 +05:30
Andrew Bastin
05f2d8817b chore: merge hoppscotch/main into release/2023.8.0 2023-08-08 12:18:01 +05:30
Joel Jacob Stephen
81fbb22c51 feat: introducing a new smart input hoppscotch ui component (#3089)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-08-05 23:45:02 +05:30
Liyas Thomas
01cf59c663 fix: disable line wrapping in EnvInput component (#3230) 2023-08-05 23:43:02 +05:30
Liyas Thomas
5c8ebaff3e refactor: fonts are now bundled with packages (#3227) 2023-08-05 23:42:31 +05:30
Andrew Bastin
0e70c28324 feat: dynamically select which auth providers for your instance of hoppscotch (be implementation) 2023-08-03 20:12:54 +05:30
Liyas Thomas
c1efa381f0 feat: svg badge asset (#3196) 2023-07-25 20:45:22 +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
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
81 changed files with 1587 additions and 1401 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"
VITE_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

@@ -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 VITE_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.VITE_ALLOWED_AUTH_PROVIDERS
? process.env.VITE_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 "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
*/
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
*/
export const ENV_EMPTY_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
*/
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
'"VITE_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,13 @@ 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 +158,31 @@ export function isValidLength(title: string, length: number) {
return true;
}
/**
* This function is called by bootstrap() in main.ts
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* If not, it throws an error.
*/
export function checkEnvironmentAuthProvider() {
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
}
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
}
const givenAuthProviders = process.env.VITE_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

@@ -1,7 +1,7 @@
@mixin base-theme {
--font-sans: "Inter", sans-serif;
--font-mono: "Roboto Mono", monospace;
--font-icon: "Material Icons";
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
}

View File

@@ -31,6 +31,7 @@
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Prettify",
"rename": "Rename",
"remove": "Remove",
"restore": "Restore",
"save": "Save",
@@ -132,6 +133,7 @@
"renamed": "Collection renamed",
"request_in_use": "Request in use",
"save_as": "Save as",
"save_to_collection": "Save to Collection",
"select": "Select a Collection",
"select_location": "Select location",
"select_team": "Select a team",
@@ -149,6 +151,7 @@
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
},
"context_menu": {
@@ -432,6 +435,7 @@
"payload": "Payload",
"query": "Query",
"raw_body": "Raw Request Body",
"rename": "Rename Request",
"renamed": "Request renamed",
"run": "Run",
"save": "Save",
@@ -658,8 +662,11 @@
"tab": {
"authorization": "Authorization",
"body": "Body",
"close": "Close Tab",
"close_others": "Close other Tabs",
"collections": "Collections",
"documentation": "Documentation",
"duplicate": "Duplicate Tab",
"environments": "Environments",
"headers": "Headers",
"history": "History",

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",
@@ -33,6 +33,9 @@
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.1.0",
"@codemirror/view": "^6.0.2",
"@fontsource-variable/inter": "^5.0.5",
"@fontsource-variable/material-symbols-rounded": "^5.0.5",
"@fontsource-variable/roboto-mono": "^5.0.6",
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
@@ -139,11 +142,11 @@
"rollup-plugin-polyfill-node": "^0.10.1",
"sass": "^1.53.0",
"typescript": "^4.5.4",
"unplugin-fonts": "^1.0.3",
"unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.21.0",
"vite": "^3.1.4",
"vite-plugin-checker": "^0.5.1",
"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",

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

@@ -1,32 +1,222 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
import "@vue/runtime-core"
export {}
declare module '@vue/runtime-core' {
declare module "@vue/runtime-core" {
export interface GlobalComponents {
AppActionHandler: typeof import("./components/app/ActionHandler.vue")["default"]
AppAnnouncement: typeof import("./components/app/Announcement.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"]
AppHeader: typeof import("./components/app/Header.vue")["default"]
AppInterceptor: typeof import("./components/app/Interceptor.vue")["default"]
AppLogo: typeof import("./components/app/Logo.vue")["default"]
AppOptions: typeof import("./components/app/Options.vue")["default"]
AppPaneLayout: typeof import("./components/app/PaneLayout.vue")["default"]
AppShare: typeof import("./components/app/Share.vue")["default"]
AppShortcuts: typeof import("./components/app/Shortcuts.vue")["default"]
AppShortcutsEntry: typeof import("./components/app/ShortcutsEntry.vue")["default"]
AppShortcutsPrompt: typeof import("./components/app/ShortcutsPrompt.vue")["default"]
AppSidenav: typeof import("./components/app/Sidenav.vue")["default"]
AppSpotlight: typeof import("./components/app/spotlight/index.vue")["default"]
AppSpotlightEntry: typeof import("./components/app/spotlight/Entry.vue")["default"]
AppSpotlightEntryGQLHistory: typeof import("./components/app/spotlight/entry/GQLHistory.vue")["default"]
AppSpotlightEntryRESTHistory: typeof import("./components/app/spotlight/entry/RESTHistory.vue")["default"]
AppSupport: typeof import("./components/app/Support.vue")["default"]
ButtonPrimary: typeof import("./../../hoppscotch-ui/src/components/button/Primary.vue")["default"]
ButtonSecondary: typeof import("./../../hoppscotch-ui/src/components/button/Secondary.vue")["default"]
Collections: typeof import("./components/collections/index.vue")["default"]
CollectionsAdd: typeof import("./components/collections/Add.vue")["default"]
CollectionsAddFolder: typeof import("./components/collections/AddFolder.vue")["default"]
CollectionsAddRequest: typeof import("./components/collections/AddRequest.vue")["default"]
CollectionsCollection: typeof import("./components/collections/Collection.vue")["default"]
CollectionsEdit: typeof import("./components/collections/Edit.vue")["default"]
CollectionsEditFolder: typeof import("./components/collections/EditFolder.vue")["default"]
CollectionsEditRequest: typeof import("./components/collections/EditRequest.vue")["default"]
CollectionsGraphql: typeof import("./components/collections/graphql/index.vue")["default"]
CollectionsGraphqlAdd: typeof import("./components/collections/graphql/Add.vue")["default"]
CollectionsGraphqlAddFolder: typeof import("./components/collections/graphql/AddFolder.vue")["default"]
CollectionsGraphqlAddRequest: typeof import("./components/collections/graphql/AddRequest.vue")["default"]
CollectionsGraphqlCollection: typeof import("./components/collections/graphql/Collection.vue")["default"]
CollectionsGraphqlEdit: typeof import("./components/collections/graphql/Edit.vue")["default"]
CollectionsGraphqlEditFolder: typeof import("./components/collections/graphql/EditFolder.vue")["default"]
CollectionsGraphqlEditRequest: typeof import("./components/collections/graphql/EditRequest.vue")["default"]
CollectionsGraphqlFolder: typeof import("./components/collections/graphql/Folder.vue")["default"]
CollectionsGraphqlImportExport: typeof import("./components/collections/graphql/ImportExport.vue")["default"]
CollectionsGraphqlRequest: typeof import("./components/collections/graphql/Request.vue")["default"]
CollectionsImportExport: typeof import("./components/collections/ImportExport.vue")["default"]
CollectionsMyCollections: typeof import("./components/collections/MyCollections.vue")["default"]
CollectionsRequest: typeof import("./components/collections/Request.vue")["default"]
CollectionsSaveRequest: typeof import("./components/collections/SaveRequest.vue")["default"]
CollectionsTeamCollections: typeof import("./components/collections/TeamCollections.vue")["default"]
Environments: typeof import("./components/environments/index.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"]
EnvironmentsMyEnvironment: typeof import("./components/environments/my/Environment.vue")["default"]
EnvironmentsSelector: typeof import("./components/environments/Selector.vue")["default"]
EnvironmentsTeams: typeof import("./components/environments/teams/index.vue")["default"]
EnvironmentsTeamsDetails: typeof import("./components/environments/teams/Details.vue")["default"]
EnvironmentsTeamsEnvironment: typeof import("./components/environments/teams/Environment.vue")["default"]
FirebaseLogin: typeof import("./components/firebase/Login.vue")["default"]
FirebaseLogout: typeof import("./components/firebase/Logout.vue")["default"]
GraphqlAuthorization: typeof import("./components/graphql/Authorization.vue")["default"]
GraphqlField: typeof import("./components/graphql/Field.vue")["default"]
GraphqlRequest: typeof import("./components/graphql/Request.vue")["default"]
GraphqlRequestOptions: typeof import("./components/graphql/RequestOptions.vue")["default"]
GraphqlResponse: typeof import("./components/graphql/Response.vue")["default"]
GraphqlSidebar: typeof import("./components/graphql/Sidebar.vue")["default"]
GraphqlType: typeof import("./components/graphql/Type.vue")["default"]
GraphqlTypeLink: typeof import("./components/graphql/TypeLink.vue")["default"]
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"]
HoppSmartInput: typeof import("@hoppscotch/ui")["HoppSmartInput"]
HoppSmartIntersection: typeof import("@hoppscotch/ui")["HoppSmartIntersection"]
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"]
HoppSmartToggle: typeof import("@hoppscotch/ui")["HoppSmartToggle"]
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"]
HttpBody: typeof import("./components/http/Body.vue")["default"]
HttpBodyParameters: typeof import("./components/http/BodyParameters.vue")["default"]
HttpCodegenModal: typeof import("./components/http/CodegenModal.vue")["default"]
HttpHeaders: typeof import("./components/http/Headers.vue")["default"]
HttpImportCurl: typeof import("./components/http/ImportCurl.vue")["default"]
HttpOAuth2Authorization: typeof import("./components/http/OAuth2Authorization.vue")["default"]
HttpParameters: typeof import("./components/http/Parameters.vue")["default"]
HttpPreRequestScript: typeof import("./components/http/PreRequestScript.vue")["default"]
HttpRawBody: typeof import("./components/http/RawBody.vue")["default"]
HttpReqChangeConfirmModal: typeof import("./components/http/ReqChangeConfirmModal.vue")["default"]
HttpRequest: typeof import("./components/http/Request.vue")["default"]
HttpRequestOptions: typeof import("./components/http/RequestOptions.vue")["default"]
HttpRequestTab: typeof import("./components/http/RequestTab.vue")["default"]
HttpResponse: typeof import("./components/http/Response.vue")["default"]
HttpResponseMeta: typeof import("./components/http/ResponseMeta.vue")["default"]
HttpSidebar: typeof import("./components/http/Sidebar.vue")["default"]
HttpTestResult: typeof import("./components/http/TestResult.vue")["default"]
HttpTestResultEntry: typeof import("./components/http/TestResultEntry.vue")["default"]
HttpTestResultEnv: typeof import("./components/http/TestResultEnv.vue")["default"]
HttpTestResultReport: typeof import("./components/http/TestResultReport.vue")["default"]
HttpTests: typeof import("./components/http/Tests.vue")["default"]
HttpURLEncodedParams: typeof import("./components/http/URLEncodedParams.vue")["default"]
IconLucideAlertTriangle: typeof import("~icons/lucide/alert-triangle")["default"]
IconLucideArrowLeft: typeof import("~icons/lucide/arrow-left")["default"]
IconLucideCheckCircle: typeof import("~icons/lucide/check-circle")["default"]
IconLucideChevronRight: typeof import("~icons/lucide/chevron-right")["default"]
IconLucideGlobe: typeof import("~icons/lucide/globe")["default"]
IconLucideHelpCircle: typeof import("~icons/lucide/help-circle")["default"]
IconLucideInbox: typeof import("~icons/lucide/inbox")["default"]
IconLucideInfo: typeof import("~icons/lucide/info")["default"]
IconLucideLayers: typeof import("~icons/lucide/layers")["default"]
IconLucideListEnd: typeof import("~icons/lucide/list-end")["default"]
IconLucideMinus: typeof import("~icons/lucide/minus")["default"]
IconLucideSearch: typeof import("~icons/lucide/search")["default"]
IconLucideUsers: typeof import("~icons/lucide/users")["default"]
IconLucideVerified: typeof import("~icons/lucide/verified")["default"]
LensesHeadersRenderer: typeof import("./components/lenses/HeadersRenderer.vue")["default"]
LensesHeadersRendererEntry: typeof import("./components/lenses/HeadersRendererEntry.vue")["default"]
LensesRenderersAudioLensRenderer: typeof import("./components/lenses/renderers/AudioLensRenderer.vue")["default"]
LensesRenderersHTMLLensRenderer: typeof import("./components/lenses/renderers/HTMLLensRenderer.vue")["default"]
LensesRenderersImageLensRenderer: typeof import("./components/lenses/renderers/ImageLensRenderer.vue")["default"]
LensesRenderersJSONLensRenderer: typeof import("./components/lenses/renderers/JSONLensRenderer.vue")["default"]
LensesRenderersPDFLensRenderer: typeof import("./components/lenses/renderers/PDFLensRenderer.vue")["default"]
LensesRenderersRawLensRenderer: typeof import("./components/lenses/renderers/RawLensRenderer.vue")["default"]
LensesRenderersVideoLensRenderer: typeof import("./components/lenses/renderers/VideoLensRenderer.vue")["default"]
LensesRenderersXMLLensRenderer: typeof import("./components/lenses/renderers/XMLLensRenderer.vue")["default"]
LensesResponseBodyRenderer: typeof import("./components/lenses/ResponseBodyRenderer.vue")["default"]
ProfileShortcode: typeof import("./components/profile/Shortcode.vue")["default"]
ProfileShortcodes: typeof import("./components/profile/Shortcodes.vue")["default"]
ProfileUserDelete: typeof import("./components/profile/UserDelete.vue")["default"]
RealtimeCommunication: typeof import("./components/realtime/Communication.vue")["default"]
RealtimeConnectionConfig: typeof import("./components/realtime/ConnectionConfig.vue")["default"]
RealtimeLog: typeof import("./components/realtime/Log.vue")["default"]
RealtimeLogEntry: typeof import("./components/realtime/LogEntry.vue")["default"]
RealtimeSubscription: typeof import("./components/realtime/Subscription.vue")["default"]
SmartAccentModePicker: typeof import("./components/smart/AccentModePicker.vue")["default"]
SmartAnchor: typeof import("./../../hoppscotch-ui/src/components/smart/Anchor.vue")["default"]
SmartAutoComplete: typeof import("./../../hoppscotch-ui/src/components/smart/AutoComplete.vue")["default"]
SmartChangeLanguage: typeof import("./components/smart/ChangeLanguage.vue")["default"]
SmartCheckbox: typeof import("./../../hoppscotch-ui/src/components/smart/Checkbox.vue")["default"]
SmartColorModePicker: typeof import("./components/smart/ColorModePicker.vue")["default"]
SmartConfirmModal: typeof import("./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue")["default"]
SmartEnvInput: typeof import("./components/smart/EnvInput.vue")["default"]
SmartExpand: typeof import("./../../hoppscotch-ui/src/components/smart/Expand.vue")["default"]
SmartFileChip: typeof import("./../../hoppscotch-ui/src/components/smart/FileChip.vue")["default"]
SmartFontSizePicker: typeof import("./components/smart/FontSizePicker.vue")["default"]
SmartInput: typeof import("./../../hoppscotch-ui/src/components/smart/Input.vue")["default"]
SmartIntersection: typeof import("./../../hoppscotch-ui/src/components/smart/Intersection.vue")["default"]
SmartItem: typeof import("./../../hoppscotch-ui/src/components/smart/Item.vue")["default"]
SmartLink: typeof import("./../../hoppscotch-ui/src/components/smart/Link.vue")["default"]
SmartModal: typeof import("./../../hoppscotch-ui/src/components/smart/Modal.vue")["default"]
SmartPicture: typeof import("./../../hoppscotch-ui/src/components/smart/Picture.vue")["default"]
SmartPlaceholder: typeof import("./../../hoppscotch-ui/src/components/smart/Placeholder.vue")["default"]
SmartProgressRing: typeof import("./../../hoppscotch-ui/src/components/smart/ProgressRing.vue")["default"]
SmartRadio: typeof import("./../../hoppscotch-ui/src/components/smart/Radio.vue")["default"]
SmartRadioGroup: typeof import("./../../hoppscotch-ui/src/components/smart/RadioGroup.vue")["default"]
SmartSlideOver: typeof import("./../../hoppscotch-ui/src/components/smart/SlideOver.vue")["default"]
SmartSpinner: typeof import("./../../hoppscotch-ui/src/components/smart/Spinner.vue")["default"]
SmartTab: typeof import("./../../hoppscotch-ui/src/components/smart/Tab.vue")["default"]
SmartTabs: typeof import("./../../hoppscotch-ui/src/components/smart/Tabs.vue")["default"]
SmartToggle: typeof import("./../../hoppscotch-ui/src/components/smart/Toggle.vue")["default"]
SmartTree: typeof import("./components/smart/Tree.vue")["default"]
SmartTreeBranch: typeof import("./components/smart/TreeBranch.vue")["default"]
SmartWindow: typeof import("./../../hoppscotch-ui/src/components/smart/Window.vue")["default"]
SmartWindows: typeof import("./../../hoppscotch-ui/src/components/smart/Windows.vue")["default"]
TabPrimary: typeof import("./components/tab/Primary.vue")["default"]
TabSecondary: typeof import("./components/tab/Secondary.vue")["default"]
Teams: typeof import("./components/teams/index.vue")["default"]
TeamsAdd: typeof import("./components/teams/Add.vue")["default"]
TeamsEdit: typeof import("./components/teams/Edit.vue")["default"]
TeamsInvite: typeof import("./components/teams/Invite.vue")["default"]
TeamsMemberStack: typeof import("./components/teams/MemberStack.vue")["default"]
TeamsModal: typeof import("./components/teams/Modal.vue")["default"]
TeamsTeam: typeof import("./components/teams/Team.vue")["default"]
Tippy: typeof import("vue-tippy")["Tippy"]
WorkspaceCurrent: typeof import("./components/workspace/Current.vue")["default"]
WorkspaceSelector: typeof import("./components/workspace/Selector.vue")["default"]
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']
AppFuse: typeof import('./components/app/Fuse.vue')['default']
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default']
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
AppLogo: typeof import('./components/app/Logo.vue')['default']
AppOptions: typeof import('./components/app/Options.vue')['default']
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
AppPowerSearch: typeof import('./components/app/PowerSearch.vue')['default']
AppPowerSearchEntry: typeof import('./components/app/PowerSearchEntry.vue')['default']
AppShare: typeof import('./components/app/Share.vue')['default']
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default']
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
@@ -55,7 +245,6 @@ 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']
@@ -85,6 +274,7 @@ declare module '@vue/runtime-core' {
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
@@ -96,6 +286,7 @@ declare module '@vue/runtime-core' {
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
@@ -117,6 +308,7 @@ declare module '@vue/runtime-core' {
HttpResponse: typeof import('./components/http/Response.vue')['default']
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
@@ -134,9 +326,9 @@ 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']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
@@ -172,7 +364,6 @@ declare module '@vue/runtime-core' {
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
@@ -198,5 +389,4 @@ declare module '@vue/runtime-core' {
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
}
}

View File

@@ -4,15 +4,13 @@
<div
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
>
<div class="flex flex-col px-6 py-4 border-b border-dividerLight">
<input
v-model="filterText"
type="search"
autocomplete="off"
class="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
:placeholder="`${t('action.search')}`"
/>
</div>
<HoppSmartInput
v-model="filterText"
type="search"
styles="px-6 py-4 border-b border-dividerLight"
:placeholder="`${t('action.search')}`"
input-styles="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
/>
</div>
<div class="flex flex-col divide-y divide-dividerLight">
<HoppSmartPlaceholder
@@ -21,6 +19,7 @@
>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</HoppSmartPlaceholder>
<details
v-for="(sectionResults, sectionTitle) in shortcutsResults"
v-else

View File

@@ -6,21 +6,13 @@
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelAdd"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addNewCollection"
/>
<label for="selectLabelAdd">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="addNewCollection"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelAddFolder"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addFolder"
/>
<label for="selectLabelAddFolder">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
input-styles="floating-input"
:label="t('action.label')"
@submit="addFolder"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,19 +6,13 @@
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelAddRequest"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addRequest"
/>
<label for="selectLabelAddRequest">{{ t("action.label") }}</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="addRequest"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelEdit"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveCollection"
/>
<label for="selectLabelEdit">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
input-styles="floating-input"
:label="t('action.label')"
@submit="saveCollection"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelEditFolder"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="editFolder"
/>
<label for="selectLabelEditFolder">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="editFolder"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelEditReq"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="editRequest"
/>
<label for="selectLabelEditReq">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="editRequest"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -8,21 +8,15 @@
>
<template #body>
<div class="flex flex-col">
<div class="relative flex">
<input
id="selectLabelSaveReq"
v-model="requestName"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveRequestAs"
/>
<label for="selectLabelSaveReq">
{{ t("request.name") }}
</label>
</div>
<HoppSmartInput
v-model="requestName"
styles="relative flex"
placeholder=" "
:label="t('request.name')"
input-styles="floating-input"
@submit="saveRequestAs"
/>
<label class="p-4">
{{ t("collection.select_location") }}
</label>
@@ -62,7 +56,7 @@
</template>
<script setup lang="ts">
import { nextTick, reactive, ref, watch } from "vue"
import { computed, nextTick, reactive, ref, watch } from "vue"
import { cloneDeep } from "lodash-es"
import {
HoppGQLRequest,
@@ -107,10 +101,12 @@ const props = withDefaults(
defineProps<{
show: boolean
mode: "rest" | "graphql"
request?: HoppRESTRequest | HoppGQLRequest | null
}>(),
{
show: false,
mode: "rest",
request: null,
}
)
@@ -132,9 +128,17 @@ const restRequestName = computedWithControl(
() => currentActiveTab.value.document.request.name
)
const requestName = ref(
props.mode === "rest" ? restRequestName.value : gqlRequestName.value
)
const reqName = computed(() => {
if (props.request) {
return props.request.name
} else if (props.mode === "rest") {
return restRequestName.value
} else {
return gqlRequestName.value
}
})
const requestName = ref(reqName.value)
watch(
() => [currentActiveTab.value, gqlRequestName.value],
@@ -198,10 +202,15 @@ const saveRequestAs = async () => {
return
}
const requestUpdated =
props.mode === "rest"
? cloneDeep(currentActiveTab.value.document.request)
: cloneDeep(getGQLSession().request)
let requestUpdated
if (props.request) {
requestUpdated = cloneDeep(props.request)
} else if (props.mode === "rest") {
requestUpdated = cloneDeep(currentActiveTab.value.document.request)
} else {
requestUpdated = cloneDeep(getGQLSession().request)
}
requestUpdated.name = requestName.value

View File

@@ -6,21 +6,13 @@
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelGqlAdd"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addNewCollection"
/>
<label for="selectLabelGqlAdd">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
input-styles="floating-input"
:label="t('action.label')"
@submit="addNewCollection"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelGqlAddFolder"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addFolder"
/>
<label for="selectLabelGqlAddFolder">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="addFolder"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelGqlAddRequest"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addRequest"
/>
<label for="selectLabelGqlAddRequest">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="addRequest"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelGqlEdit"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveCollection"
/>
<label for="selectLabelGqlEdit">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="saveCollection"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelGqlEditFolder"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="editFolder"
/>
<label for="selectLabelGqlEditFolder">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="editFolder"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,21 +6,13 @@
@close="hideModal"
>
<template #body>
<div class="flex flex-col">
<input
id="selectLabelGqlEditReq"
v-model="requestUpdateData.name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveRequest"
/>
<label for="selectLabelGqlEditReq">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="requestUpdateData.name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="saveRequest"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -11,7 +11,7 @@
type="search"
autocomplete="off"
:placeholder="t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent"
class="py-2 pl-4 pr-2 bg-transparent !border-0"
/>
<div
class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"

View File

@@ -18,12 +18,12 @@
"
>
<WorkspaceCurrent :section="t('tab.collections')" />
<input
<HoppSmartInput
v-model="filterTexts"
type="search"
autocomplete="off"
:placeholder="t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent"
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
type="search"
:disabled="collectionsType.type === 'team-collections'"
/>
</div>

View File

@@ -7,22 +7,15 @@
>
<template #body>
<div class="flex flex-col">
<div class="relative flex">
<input
id="selectLabelEnvEdit"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
:disabled="editingEnvironmentIndex === 'Global'"
@keyup.enter="saveEnvironment"
/>
<label for="selectLabelEnvEdit">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
:disabled="editingEnvironmentIndex === 'Global'"
@submit="saveEnvironment"
/>
<div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}

View File

@@ -7,23 +7,15 @@
>
<template #body>
<div class="flex flex-col px-2">
<div class="relative flex">
<input
id="selectLabelEnvEdit"
v-model="name"
v-focus
class="input floating-input"
:class="isViewer && 'opacity-25'"
placeholder=""
type="text"
autocomplete="off"
:disabled="isViewer"
@keyup.enter="saveEnvironment"
/>
<label for="selectLabelEnvEdit">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:input-styles="['floating-input', isViewer && 'opacity-25']"
:label="t('action.label')"
:disabled="isViewer"
@submit="saveEnvironment"
/>
<div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}

View File

@@ -37,24 +37,14 @@
class="flex flex-col space-y-2"
@submit.prevent="signInWithEmail"
>
<div class="flex flex-col">
<input
id="email"
v-model="form.email"
v-focus
class="input floating-input"
placeholder=" "
type="email"
name="email"
autocomplete="off"
required
spellcheck="false"
autofocus
/>
<label for="email">
{{ t("auth.email") }}
</label>
</div>
<HoppSmartInput
v-model="form.email"
type="email"
placeholder=" "
:label="t('auth.email')"
input-styles="floating-input"
/>
<HoppButtonPrimary
:loading="signingInWithEmail"
type="submit"

View File

@@ -105,6 +105,7 @@
@toggle-star="toggleStar(entry.entry)"
@delete-entry="deleteHistory(entry.entry)"
@use-entry="useHistory(toRaw(entry.entry))"
@add-to-collection="addToCollection(entry.entry)"
/>
</details>
</div>
@@ -176,7 +177,7 @@ import {
import HistoryRestCard from "./rest/Card.vue"
import HistoryGraphqlCard from "./graphql/Card.vue"
import { createNewTab } from "~/helpers/rest/tab"
import { defineActionHandler } from "~/helpers/actions"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
@@ -324,6 +325,14 @@ const deleteHistory = (entry: HistoryEntry) => {
toast.success(`${t("state.deleted")}`)
}
const addToCollection = (entry: HistoryEntry) => {
if (props.page === "rest") {
invokeAction("request.save-as", {
request: entry.request,
})
}
}
const toggleStar = (entry: HistoryEntry) => {
// History entry type specified because function does not know the type
if (props.page === "rest")

View File

@@ -1,5 +1,8 @@
<template>
<div class="flex items-stretch group">
<div
class="flex items-stretch group"
@contextmenu.prevent="options!.tippy.show()"
>
<span
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
@@ -26,6 +29,39 @@
{{ entry.request.endpoint }}
</span>
</span>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.s="addToCollectionAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="addToCollectionAction"
:icon="IconSave"
:label="`${t('collection.save_to_collection')}`"
:shortcut="['S']"
@click="
() => {
emit('add-to-collection')
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconTrash"
@@ -48,15 +84,16 @@
</template>
<script setup lang="ts">
import { computed } from "vue"
import { computed, ref } from "vue"
import findStatusGroup from "~/helpers/findStatusGroup"
import { useI18n } from "@composables/i18n"
import { RESTHistoryEntry } from "~/newstore/history"
import { shortDateTime } from "~/helpers/utils/date"
import IconSave from "~icons/lucide/save"
import IconStar from "~icons/lucide/star"
import IconStarOff from "~icons/hopp/star-off"
import IconTrash from "~icons/lucide/trash"
import { TippyComponent } from "vue-tippy"
const props = defineProps<{
entry: RESTHistoryEntry
@@ -67,8 +104,13 @@ const emit = defineEmits<{
(e: "use-entry"): void
(e: "delete-entry"): void
(e: "toggle-star"): void
(e: "add-to-collection"): void
}>()
const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null)
const addToCollectionAction = ref<HTMLButtonElement | null>(null)
const t = useI18n()
const duration = computed(() => {

View File

@@ -221,6 +221,7 @@
v-if="showSaveRequestModal"
mode="rest"
:show="showSaveRequestModal"
:request="request"
@hide-modal="showSaveRequestModal = false"
/>
</div>
@@ -263,6 +264,7 @@ import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
import { platform } from "~/platform"
import { getCurrentStrategyID } from "~/helpers/network"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
const t = useI18n()
@@ -578,6 +580,8 @@ const saveRequest = () => {
}
}
const request = ref<HoppRESTRequest | null>(null)
onBeforeUnmount(() => {
if (loading.value) cancelRequest()
})
@@ -593,7 +597,22 @@ defineActionHandler("request.method.prev", cycleUpMethod)
defineActionHandler("request.save", saveRequest)
defineActionHandler(
"request.save-as",
() => (showSaveRequestModal.value = true)
(
req:
| {
requestType: "rest"
request: HoppRESTRequest
}
| {
requestType: "gql"
request: HoppGQLRequest
}
) => {
showSaveRequestModal.value = true
if (req && req.requestType === "rest") {
request.value = req.request
}
}
)
defineActionHandler("request.method.get", () => updateMethod("GET"))
defineActionHandler("request.method.post", () => updateMethod("POST"))

View File

@@ -0,0 +1,126 @@
<template>
<div
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="tab.document.request.name"
class="truncate px-2 flex items-center"
@dblclick="emit('open-rename-modal')"
@contextmenu.prevent="options?.tippy.show()"
@click.middle="emit('close-tab')"
>
<span
class="font-semibold text-tiny"
:class="getMethodLabelColorClassOf(tab.document.request)"
>
{{ tab.document.request.method }}
</span>
<tippy
ref="options"
trigger="manual"
interactive
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span class="leading-8 px-2">
{{ tab.document.request.name }}
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.r="renameAction?.$el.click()"
@keyup.d="duplicateAction?.$el.click()"
@keyup.w="closeAction?.$el.click()"
@keyup.x="closeOthersAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="renameAction"
:icon="IconFileEdit"
:label="t('request.rename')"
:shortcut="['R']"
@click="
() => {
emit('open-rename-modal')
hide()
}
"
/>
<HoppSmartItem
ref="duplicateAction"
:icon="IconCopy"
:label="t('tab.duplicate')"
:shortcut="['D']"
@click="
() => {
emit('duplicate-tab')
hide()
}
"
/>
<HoppSmartItem
v-if="isRemovable"
ref="closeAction"
:icon="IconXCircle"
:label="t('tab.close')"
:shortcut="['W']"
@click="
() => {
emit('close-tab')
hide()
}
"
/>
<HoppSmartItem
v-if="isRemovable"
ref="closeOthersAction"
:icon="IconXSquare"
:label="t('tab.close_others')"
:shortcut="['X']"
@click="
() => {
emit('close-other-tabs')
hide()
}
"
/>
</div>
</template>
</tippy>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { TippyComponent } from "vue-tippy"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { useI18n } from "~/composables/i18n"
import { HoppRESTTab } from "~/helpers/rest/tab"
import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy"
const t = useI18n()
defineProps<{
tab: HoppRESTTab
isRemovable: boolean
}>()
const emit = defineEmits<{
(event: "open-rename-modal"): void
(event: "close-tab"): void
(event: "close-other-tabs"): void
(event: "duplicate-tab"): void
}>()
const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null)
const renameAction = ref<HTMLButtonElement | null>(null)
const closeAction = ref<HTMLButtonElement | null>(null)
const closeOthersAction = ref<HTMLButtonElement | null>(null)
const duplicateAction = ref<HTMLButtonElement | null>(null)
</script>

View File

@@ -18,15 +18,13 @@
</span>
<template #content="{ hide }">
<div class="flex flex-col space-y-2">
<div class="sticky z-10 top-0 flex-shrink-0 overflow-x-auto">
<input
v-model="searchQuery"
type="search"
autocomplete="off"
class="flex w-full p-4 py-2 input !bg-primaryContrast"
:placeholder="`${t('action.search')}`"
/>
</div>
<HoppSmartInput
v-model="searchQuery"
styles="ticky z-10 top-0 flex-shrink-0 overflow-x-auto"
:placeholder="`${t('action.search')}`"
type="search"
input-styles="flex w-full p-4 py-2 input !bg-primaryContrast"
/>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"

View File

@@ -342,7 +342,6 @@ const initView = (el: any) => {
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) => {

View File

@@ -1,21 +1,13 @@
<template>
<HoppSmartModal v-if="show" dialog :title="t('team.new')" @close="hideModal">
<template #body>
<div class="flex flex-col">
<input
id="selectLabelTeamAdd"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addNewTeam"
/>
<label for="selectLabelTeamAdd">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
:label="t('action.label')"
placeholder=" "
input-styles="floating-input"
@submit="addNewTeam"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -2,21 +2,13 @@
<HoppSmartModal v-if="show" dialog :title="t('team.edit')" @close="hideModal">
<template #body>
<div class="flex flex-col">
<div class="relative flex">
<input
id="selectLabelTeamEdit"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveTeam"
/>
<label for="selectLabelTeamEdit">
{{ t("action.label") }}
</label>
</div>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="saveTeam"
/>
<div class="flex items-center justify-between flex-1 pt-4">
<label for="memberList" class="p-4">
{{ t("team.members") }}

View File

@@ -5,7 +5,7 @@
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
import { BehaviorSubject } from "rxjs"
import { HoppRESTDocument } from "./rest/document"
import { HoppGQLRequest } from "@hoppscotch/data"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
export type HoppAction =
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
@@ -76,6 +76,15 @@ type HoppActionArgsMap = {
"rest.request.open": {
doc: HoppRESTDocument
}
"request.save-as":
| {
requestType: "rest"
request: HoppRESTRequest
}
| {
requestType: "gql"
request: HoppGQLRequest
}
"gql.request.open": {
request: HoppGQLRequest
}

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

@@ -181,6 +181,33 @@ export function closeTab(tabID: string) {
tabMap.delete(tabID)
}
export function closeOtherTabs(tabID: string) {
if (!tabMap.has(tabID)) {
console.warn(
`The tab to close other tabs does not exist (tab id: ${tabID})`
)
return
}
tabOrdering.value = [tabID]
tabMap.forEach((_, id) => {
if (id !== tabID) tabMap.delete(id)
})
currentTabID.value = tabID
}
export function getDirtyTabsCount() {
let count = 0
for (const tab of tabMap.values()) {
if (tab.document.isDirty) count++
}
return count
}
export function getTabRefWithSaveContext(ctx: HoppRESTSaveContext) {
for (const tab of tabMap.values()) {
// For `team-collection` request id can be considered unique

View File

@@ -10,6 +10,9 @@ import "virtual:windi.css"
import "../assets/scss/themes.scss"
import "../assets/scss/styles.scss"
import "nprogress/nprogress.css"
import "@fontsource-variable/inter"
import "@fontsource-variable/material-symbols-rounded"
import "@fontsource-variable/roboto-mono"
import App from "./App.vue"

View File

@@ -19,22 +19,14 @@
:close-visibility="'hover'"
>
<template #tabhead>
<div
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="tab.document.request.name"
class="truncate px-2"
@dblclick="openReqRenameModal()"
>
<span
class="font-semibold text-tiny"
:class="getMethodLabelColorClassOf(tab.document.request)"
>
{{ tab.document.request.method }}
</span>
<span class="leading-8 px-2">
{{ tab.document.request.name }}
</span>
</div>
<HttpTabHead
:tab="tab"
:is-removable="tabs.length > 1"
@open-rename-modal="openReqRenameModal(tab.id)"
@close-tab="removeTab(tab.id)"
@close-other-tabs="closeOtherTabsAction(tab.id)"
@duplicate-tab="duplicateTab(tab.id)"
/>
</template>
<template #suffix>
<span
@@ -78,6 +70,13 @@
@hide-modal="onCloseConfirmSaveTab"
@resolve="onResolveConfirmSaveTab"
/>
<HoppSmartConfirmModal
:show="confirmingCloseAllTabs"
:confirm="t('modal.close_unsaved_tab')"
:title="t('confirm.close_unsaved_tabs', { count: unsavedTabsCount })"
@hide-modal="confirmingCloseAllTabs = false"
@resolve="onResolveConfirmCloseAllTabs"
/>
<CollectionsSaveRequest
v-if="savingRequest"
mode="rest"
@@ -99,10 +98,10 @@ import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { useRoute } from "vue-router"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { useI18n } from "@composables/i18n"
import {
closeTab,
closeOtherTabs,
createNewTab,
currentActiveTab,
currentTabID,
@@ -113,6 +112,7 @@ import {
persistableTabState,
updateTab,
updateTabOrdering,
getDirtyTabsCount,
} from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
@@ -139,8 +139,11 @@ import {
const savingRequest = ref(false)
const confirmingCloseForTabID = ref<string | null>(null)
const confirmingCloseAllTabs = ref(false)
const showRenamingReqNameModal = ref(false)
const reqName = ref<string>("")
const unsavedTabsCount = ref(0)
const exceptedTabID = ref<string | null>(null)
const t = useI18n()
const toast = useToast()
@@ -215,9 +218,42 @@ const removeTab = (tabID: string) => {
}
}
const openReqRenameModal = () => {
const closeOtherTabsAction = (tabID: string) => {
const dirtyTabCount = getDirtyTabsCount()
// If there are dirty tabs, show the confirm modal
if (dirtyTabCount > 0) {
confirmingCloseAllTabs.value = true
unsavedTabsCount.value = dirtyTabCount
exceptedTabID.value = tabID
} else {
closeOtherTabs(tabID)
}
}
const duplicateTab = (tabID: string) => {
const tab = getTabRef(tabID)
if (tab.value) {
const newTab = createNewTab({
request: tab.value.document.request,
isDirty: true,
})
currentTabID.value = newTab.id
}
}
const onResolveConfirmCloseAllTabs = () => {
if (exceptedTabID.value) closeOtherTabs(exceptedTabID.value)
confirmingCloseAllTabs.value = false
}
const openReqRenameModal = (tabID?: string) => {
if (tabID) {
const tab = getTabRef(tabID)
reqName.value = tab.value.document.request.name
} else {
reqName.value = currentActiveTab.value.document.request.name
}
showRenamingReqNameModal.value = true
reqName.value = currentActiveTab.value.document.request.name
}
const renameReqName = () => {

View File

@@ -100,55 +100,45 @@
<label for="displayName">
{{ t("settings.profile_name") }}
</label>
<form
class="flex mt-2 md:max-w-sm"
@submit.prevent="updateDisplayName"
<HoppSmartInput
v-model="displayName"
styles="mt-2 md:max-w-sm"
:placeholder="`${t('settings.profile_name')}`"
>
<input
id="displayName"
v-model="displayName"
class="input"
:placeholder="`${t('settings.profile_name')}`"
type="text"
autocomplete="off"
required
/>
<HoppButtonSecondary
filled
outline
:label="t('action.save')"
class="ml-2 min-w-16"
type="submit"
:loading="updatingDisplayName"
/>
</form>
<template #button>
<HoppButtonSecondary
filled
outline
:label="t('action.save')"
class="ml-2 min-w-16"
type="submit"
:loading="updatingDisplayName"
@click="updateDisplayName"
/>
</template>
</HoppSmartInput>
</div>
<div class="py-4">
<label for="emailAddress">
{{ t("settings.profile_email") }}
</label>
<form
class="flex mt-2 md:max-w-sm"
@submit.prevent="updateEmailAddress"
<HoppSmartInput
v-model="emailAddress"
styles="flex mt-2 md:max-w-sm"
:placeholder="`${t('settings.profile_name')}`"
>
<input
id="emailAddress"
v-model="emailAddress"
class="input"
:placeholder="`${t('settings.profile_name')}`"
type="email"
autocomplete="off"
required
/>
<HoppButtonSecondary
filled
outline
:label="t('action.save')"
class="ml-2 min-w-16"
type="submit"
:loading="updatingEmailAddress"
/>
</form>
<template #button>
<HoppButtonSecondary
filled
outline
:label="t('action.save')"
class="ml-2 min-w-16"
type="submit"
:loading="updatingEmailAddress"
@click="updateEmailAddress"
/>
</template>
</HoppSmartInput>
</div>
</section>
@@ -208,7 +198,6 @@ import { ref, watchEffect, computed } from "vue"
import { platform } from "~/platform"
import { invokeAction } from "~/helpers/actions"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
@@ -251,9 +240,9 @@ const loadingCurrentUser = computed(() => {
else return false
})
const displayName = ref(currentUser.value?.displayName)
const displayName = ref(currentUser.value?.displayName || "")
const updatingDisplayName = ref(false)
watchEffect(() => (displayName.value = currentUser.value?.displayName))
watchEffect(() => (displayName.value = currentUser.value?.displayName || ""))
const updateDisplayName = () => {
updatingDisplayName.value = true
@@ -270,9 +259,9 @@ const updateDisplayName = () => {
})
}
const emailAddress = ref(currentUser.value?.email)
const emailAddress = ref(currentUser.value?.email || "")
const updatingEmailAddress = ref(false)
watchEffect(() => (emailAddress.value = currentUser.value?.email))
watchEffect(() => (emailAddress.value = currentUser.value?.email || ""))
const updateEmailAddress = () => {
updatingEmailAddress.value = true

View File

@@ -4,38 +4,35 @@
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 space-x-2 overflow-x-auto bg-primary"
>
<div class="inline-flex flex-1 space-x-2">
<input
id="websocket-url"
v-model="url"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
type="url"
autocomplete="off"
spellcheck="false"
:class="{ error: !isUrlValid }"
:placeholder="`${t('websocket.url')}`"
:disabled="
connectionState === 'CONNECTED' ||
connectionState === 'CONNECTING'
"
@keyup.enter="isUrlValid ? toggleConnection() : null"
/>
<HoppButtonPrimary
id="connect"
:disabled="!isUrlValid"
class="w-32"
name="connect"
:label="
connectionState === 'CONNECTING'
? t('action.connecting')
: connectionState === 'DISCONNECTED'
? t('action.connect')
: t('action.disconnect')
"
:loading="connectionState === 'CONNECTING'"
@click="toggleConnection"
/>
</div>
<HoppSmartInput
v-model="url"
type="url"
styles="!inline-flex flex-1 space-x-2"
input-styles="w-full px-4 py-2 border rounded !bg-primaryLight border-divider text-secondaryDark"
:placeholder="`${t('websocket.url')}`"
:disabled="
connectionState === 'CONNECTED' || connectionState === 'CONNECTING'
"
@submit="isUrlValid ? toggleConnection() : null"
>
<template #button>
<HoppButtonPrimary
id="connect"
:disabled="!isUrlValid"
class="w-32"
name="connect"
:label="
connectionState === 'CONNECTING'
? t('action.connecting')
: connectionState === 'DISCONNECTED'
? t('action.connect')
: t('action.disconnect')
"
:loading="connectionState === 'CONNECTING'"
@click="toggleConnection"
/>
</template>
</HoppSmartInput>
</div>
<HoppSmartTabs
v-model="selectedTab"

View File

@@ -193,20 +193,19 @@
</div>
</div>
<div class="flex items-center py-4 space-x-2">
<div class="relative flex flex-col flex-1">
<input
id="url"
v-model="PROXY_URL"
class="input floating-input"
placeholder=" "
type="url"
autocomplete="off"
:disabled="!PROXY_ENABLED"
/>
<label for="url">
{{ t("settings.proxy_url") }}
</label>
</div>
<HoppSmartInput
v-model="PROXY_URL"
styles="flex-1"
placeholder=" "
input-styles="input floating-input"
:disabled="!PROXY_ENABLED"
>
<template #label>
<label for="url">
{{ t("settings.proxy_url") }}
</label>
</template>
</HoppSmartInput>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('settings.reset_default')"

View File

@@ -15,17 +15,18 @@
"skipLibCheck": true,
"noUnusedLocals": true,
"paths": {
"~/*": [ "./src/*" ],
"@composables/*": [ "./src/composables/*" ],
"@components/*": [ "./src/components/*" ],
"@helpers/*": [ "./src/helpers/*" ],
"@modules/*": [ "./src/modules/*" ],
"@workers/*": [ "./src/workers/*" ],
"@functional/*": [ "./src/helpers/functional/*" ]
"~/*": ["./src/*"],
"@composables/*": ["./src/composables/*"],
"@components/*": ["./src/components/*"],
"@helpers/*": ["./src/helpers/*"],
"@modules/*": ["./src/modules/*"],
"@workers/*": ["./src/workers/*"],
"@functional/*": ["./src/helpers/functional/*"]
},
"types": [
"vite/client",
"unplugin-icons/types/vue",
"unplugin-fonts/client",
"vite-plugin-pages/client",
"vite-plugin-vue-layouts/client",
"vite-plugin-pwa/client"

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",
@@ -23,6 +23,9 @@
"postinstall": "pnpm run gql-codegen"
},
"dependencies": {
"@fontsource-variable/inter": "^5.0.5",
"@fontsource-variable/material-symbols-rounded": "^5.0.5",
"@fontsource-variable/roboto-mono": "^5.0.6",
"@hoppscotch/common": "workspace:^",
"@hoppscotch/data": "workspace:^",
"axios": "^0.21.4",
@@ -59,10 +62,10 @@
"eslint-plugin-vue": "^9.5.1",
"npm-run-all": "^4.1.5",
"typescript": "^4.6.4",
"unplugin-fonts": "^1.0.3",
"unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.21.0",
"vite": "^3.2.3",
"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",

View File

@@ -13,12 +13,11 @@
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@hoppscotch/common": [ "../hoppscotch-common/src/index.ts" ],
"@hoppscotch/common/*": [ "../hoppscotch-common/src/*" ],
"@hoppscotch/common": ["../hoppscotch-common/src/index.ts"],
"@hoppscotch/common/*": ["../hoppscotch-common/src/*"],
"@platform/*": ["./src/platform/*"],
"@lib/*": ["./src/lib/*"],
"@lib/*": ["./src/lib/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -15,7 +15,7 @@ import Layouts from "vite-plugin-vue-layouts"
import IconResolver from "unplugin-icons/resolver"
import { FileSystemIconLoader } from "unplugin-icons/loaders"
import * as path from "path"
import { VitePluginFonts } from "vite-plugin-fonts"
import Unfonts from "unplugin-fonts/vite"
import legacy from "@vitejs/plugin-legacy"
const ENV = loadEnv("development", path.resolve(__dirname, "../../"))
@@ -217,12 +217,21 @@ export default defineConfig({
],
},
}),
VitePluginFonts({
google: {
Unfonts({
fontsource: {
families: [
"Inter:wght@400;500;600;700;800",
"Roboto+Mono:wght@400;500",
"Material+Icons",
{
name: "Inter Variable",
variables: ["variable-full"],
},
{
name: "Material Symbols Rounded Variable",
variables: ["variable-full"],
},
{
name: "Roboto Mono Variable",
variables: ["variable-full"],
},
],
},
}),

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

@@ -1,7 +1,7 @@
@mixin base-theme {
--font-sans: 'Inter', sans-serif;
--font-mono: 'Roboto Mono', monospace;
--font-icon: 'Material Icons';
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
}

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:*",
@@ -13,6 +13,9 @@
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/inter": "^5.0.5",
"@fontsource-variable/material-symbols-rounded": "^5.0.5",
"@fontsource-variable/roboto-mono": "^5.0.6",
"@graphql-typed-document-node/core": "^3.1.1",
"@hoppscotch/ui": "workspace:^",
"@hoppscotch/vue-toasted": "^0.1.0",
@@ -56,6 +59,7 @@
"sass": "^1.57.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.3",
"unplugin-fonts": "^1.0.3",
"vite": "^3.1.4",
"vite-plugin-pages": "^0.26.0",
"vite-plugin-vue-layouts": "^0.7.0",

View File

@@ -14,6 +14,22 @@ declare module '@vue/runtime-core' {
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
AppToast: typeof import('./components/app/Toast.vue')['default']
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideUser: typeof import('~icons/lucide/user')['default']
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']

View File

@@ -41,22 +41,14 @@
class="flex flex-col space-y-4"
@submit.prevent="signInWithEmail"
>
<div class="flex flex-col">
<input
id="email"
v-model="form.email"
class="input floating-input"
placeholder=" "
type="email"
name="email"
autocomplete="off"
required
spellcheck="false"
v-focus
autofocus
/>
<label for="email"> Email </label>
</div>
<HoppSmartInput
v-model="form.email"
type="email"
placeholder=" "
input-styles="floating-input"
label="Email"
/>
<HoppButtonPrimary
:loading="signingInWithEmail"
type="submit"

View File

@@ -18,18 +18,8 @@
@input="(email: string) => getOwnerEmail(email)"
/>
</div>
<div class="flex flex-col">
<label for="teamName" class="py-2">{{ t('teams.name') }} </label>
<input
id="teamName"
v-model="teamName"
v-focus
class="input relative"
placeholder=""
type="email"
autocomplete="off"
/>
</div>
<label for="teamName"> {{ t('teams.name') }} </label>
<HoppSmartInput v-model="teamName" placeholder="" class="!my-2" />
</div>
</template>
<template #footer>

View File

@@ -20,24 +20,26 @@
'!border-accent': showRenameInput,
}"
>
<input
class="bg-transparent flex-1 p-3 rounded-md !rounded-r-none disabled:select-none border-r-0 disabled:cursor-default disabled:opacity-50"
type="text"
<HoppSmartInput
v-model="newTeamName"
styles="bg-transparent flex-1 rounded-md !rounded-r-none disabled:select-none border-r-0 disabled:cursor-default disabled:opacity-50"
placeholder="Team Name"
autofocus
:disabled="!showRenameInput"
v-focus
/>
<HoppButtonPrimary
class="!rounded-l-none"
filled
:icon="showRenameInput ? IconSave : IconEdit"
:label="
showRenameInput ? `${t('teams.rename')}` : `${t('teams.edit')}`
"
@click="handleNameEdit()"
/>
>
<template #button>
<HoppButtonPrimary
class="!rounded-l-none"
filled
:icon="showRenameInput ? IconSave : IconEdit"
:label="
showRenameInput
? `${t('teams.rename')}`
: `${t('teams.edit')}`
"
@click="handleNameEdit()"
/>
</template>
</HoppSmartInput>
</div>
</div>

View File

@@ -6,19 +6,11 @@
@close="$emit('hide-modal')"
>
<template #body>
<div class="flex flex-col">
<input
id="inviteUserEmail"
v-model="email"
v-focus
class="input floating-input"
placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="sendInvite"
/>
<label for="inviteUserEmail">{{ t('users.email_address') }}</label>
</div>
<HoppSmartInput
v-model="email"
:label="t('users.email_address')"
input-styles="floating-input"
/>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -7,6 +7,9 @@ import 'virtual:windi.css';
import '@hoppscotch/ui/style.css';
import '../assets/scss/themes.scss';
import '../assets/scss/styles.scss';
import '@fontsource-variable/inter';
import '@fontsource-variable/material-symbols-rounded';
import '@fontsource-variable/roboto-mono';
// END STYLES
import { HOPP_MODULES } from './modules';

View File

@@ -25,6 +25,7 @@
"types": [
"vite/client",
"unplugin-icons/types/vue",
"unplugin-fonts/client",
"vite-plugin-pages/client",
"vite-plugin-vue-layouts/client"
]

View File

@@ -2,6 +2,7 @@ import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
import Icons from 'unplugin-icons/vite';
import Unfonts from "unplugin-fonts/vite";
import IconResolver from 'unplugin-icons/resolver';
import Components from 'unplugin-vue-components/vite';
import WindiCSS from 'vite-plugin-windicss';
@@ -68,5 +69,23 @@ export default defineConfig({
auth: FileSystemIconLoader('../hoppscotch-sh-admin/assets/icons/auth'),
},
}),
Unfonts({
fontsource: {
families: [
{
name: "Inter Variable",
variables: ["variable-full"],
},
{
name: "Material Symbols Rounded Variable",
variables: ["variable-full"],
},
{
name: "Roboto Mono Variable",
variables: ["variable-full"],
},
],
},
}),
],
});

View File

@@ -1,5 +1,8 @@
import "./src/assets/scss/styles.scss"
import "./src/assets/scss/themes.scss"
import "virtual:windi.css"
import "@fontsource-variable/inter"
import "@fontsource-variable/material-symbols-rounded"
import "@fontsource-variable/roboto-mono"
export function setupVue3() {}

View File

@@ -18,6 +18,9 @@
"vue-router": "^4.0.16"
},
"dependencies": {
"@fontsource-variable/inter": "^5.0.5",
"@fontsource-variable/material-symbols-rounded": "^5.0.5",
"@fontsource-variable/roboto-mono": "^5.0.6",
"@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "^1.0.0",
"@vitejs/plugin-legacy": "^2.3.0",
@@ -67,11 +70,12 @@
"rollup-plugin-polyfill-node": "^0.10.1",
"sass": "^1.53.0",
"typescript": "^4.5.4",
"unplugin-fonts": "^1.0.3",
"unplugin-icons": "^0.15.3",
"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",

View File

@@ -1,7 +1,7 @@
@mixin base-theme {
--font-sans: "Inter", sans-serif;
--font-mono: "Roboto Mono", monospace;
--font-icon: "Material Icons";
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
}

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

@@ -0,0 +1,69 @@
<template>
<div class="relative flex" :class="styles">
<input
:id="inputID"
class="input"
:class="inputStyles"
v-model="inputText"
v-focus
:placeholder="placeholder"
:type="type"
@keyup.enter="emit('submit')"
autocomplete="off"
required
:disabled="disabled"
/>
<label v-if="label.length > 0" :for="inputID"> {{ label }} </label>
<slot name="button"></slot>
</div>
</template>
<script lang="ts">
/*
This inputIDCounter is tracked in the global scope in order to ensure that each input has a unique ID.
When we use this component multiple times on the same page, we need to ensure that each input has a unique ID.
This is because the label's for attribute needs to match the input's id attribute.
That's why we use a global counter that increments each time we use this component.
*/
let inputIDCounter = 564275
</script>
<script setup lang="ts">
import { useVModel } from "@vueuse/core"
import { defineProps } from "vue"
// Unique ID for input
const inputID = `input-${inputIDCounter++}`
const props = withDefaults(
defineProps<{
id: string
styles: string
modelValue: string | null
placeholder: string
inputStyles: string | (string | false)[]
type: string
label: string
disabled: boolean
}>(),
{
id: "",
styles: "",
modelValue: "",
placeholder: "",
inputStyles: "input",
type: "text",
label: "",
disabled: false,
}
)
const emit = defineEmits<{
(e: "submit"): void
(e: "update:modelValue", v: string): void
}>()
const inputText = useVModel(props, "modelValue", emit)
</script>

View File

@@ -4,6 +4,7 @@ export { default as HoppSmartCheckbox } from "./Checkbox.vue"
export { default as HoppSmartConfirmModal } from "./ConfirmModal.vue"
export { default as HoppSmartExpand } from "./Expand.vue"
export { default as HoppSmartFileChip } from "./FileChip.vue"
export { default as HoppSmartInput } from "./Input.vue"
export { default as HoppSmartIntersection } from "./Intersection.vue"
export { default as HoppSmartItem } from "./Item.vue"
export { default as HoppSmartLink } from "./Link.vue"

View File

@@ -11,47 +11,26 @@
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": [
"ESNext",
"DOM"
],
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noUnusedLocals": true,
"paths": {
"~/*": [
"./src/*"
],
"@composables/*": [
"./src/composables/*"
],
"@components/*": [
"./src/components/*"
],
"@helpers/*": [
"./src/helpers/*"
],
"@modules/*": [
"./src/modules/*"
],
"@workers/*": [
"./src/workers/*"
],
"@functional/*": [
"./src/helpers/functional/*"
]
"~/*": ["./src/*"],
"@composables/*": ["./src/composables/*"],
"@components/*": ["./src/components/*"],
"@helpers/*": ["./src/helpers/*"],
"@modules/*": ["./src/modules/*"],
"@workers/*": ["./src/workers/*"],
"@functional/*": ["./src/helpers/functional/*"]
},
"types": [
"vite/client",
"unplugin-icons/types/vue",
"unplugin-fonts/client",
"vite-plugin-pages/client",
"vite-plugin-vue-layouts/client",
"vite-plugin-pwa/client"
]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -4,28 +4,36 @@ import path from "path"
import Icons from "unplugin-icons/vite"
import { defineConfig } from "vite"
import WindiCSS from "vite-plugin-windicss"
import { VitePluginFonts } from "vite-plugin-fonts"
import Unfonts from "unplugin-fonts/vite"
export default defineConfig({
plugins: [
vue(),
dts({
insertTypesEntry: true,
skipDiagnostics: true,
outputDir: ['dist']
outDir: ["dist"],
}),
WindiCSS({
root: path.resolve(__dirname),
}),
Icons({
compiler: "vue3"
compiler: "vue3",
}),
VitePluginFonts({
google: {
Unfonts({
fontsource: {
families: [
"Inter:wght@400;500;600;700;800",
"Roboto+Mono:wght@400;500",
"Material+Icons",
{
name: "Inter Variable",
variables: ["variable-full"],
},
{
name: "Material Symbols Rounded Variable",
variables: ["variable-full"],
},
{
name: "Roboto Mono Variable",
variables: ["variable-full"],
},
],
},
}),
@@ -45,6 +53,6 @@ export default defineConfig({
exports: "named",
},
},
emptyOutDir: true
emptyOutDir: true,
},
})

997
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff