Compare commits

..

15 Commits

Author SHA1 Message Date
Balu Babu
9eb067feaf chore: fixed issues with pnpm-lock file 2023-08-03 19:39:35 +05:30
Balu Babu
0b5f57436f chore: modified error code for magic-link provider check 2023-08-03 19:36:41 +05:30
Mir Arif Hasan
0c0ed5610e chore: env check func moved to utils file 2023-08-03 19:36:32 +05:30
Mir Arif Hasan
dce032a275 feat: feedback applied 2023-08-03 19:36:17 +05:30
Mir Arif Hasan
2599b1d326 chore: check added if ALLOWED_AUTH_PROVIDERS is there in the env file or not 2023-08-03 19:35:40 +05:30
Mir Arif Hasan
419e376f46 fix: provider return type in SSO guards 2023-08-03 19:35:21 +05:30
Mir Arif Hasan
c79fcbeceb chore: handled internal server error for missing auth providers 2023-08-03 19:35:14 +05:30
Mir Arif Hasan
092cb4c3a5 chore: auth provider name read from enum 2023-08-03 19:35:06 +05:30
Mir Arif Hasan
d3f25361f7 chore: removed unused imports 2023-08-03 19:34:57 +05:30
Mir Arif Hasan
09c13e86b2 feat: remove EmptyClassProvider class 2023-08-03 19:34:48 +05:30
Balu Babu
04bb219c12 chore: fixed mistake in AUTH_PROVIDER_NOT_SPECIFIED error description 2023-08-03 19:34:40 +05:30
Balu Babu
ca79cf40b1 chore: changed target of hoppscotch-backend service back to prod in docker.compose file 2023-08-03 19:34:32 +05:30
Balu Babu
454c82975e chore: added comments to authProviderCheck function in auth/helper.ts 2023-08-03 19:34:24 +05:30
Balu Babu
c38488dfc4 feat: magic-link can now be conditionally provisioned 2023-08-03 19:34:16 +05:30
Balu Babu
2d0ebedbbb feat: social auth providers can now be conditionally provisioned 2023-08-03 19:34:07 +05:30
201 changed files with 7420 additions and 13953 deletions

View File

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

View File

@@ -13,7 +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
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, "release/**"]
branches: [main, staging]
pull_request:
branches: [main, staging, "release/**"]
branches: [main, staging]
jobs:
test:

View File

@@ -1,8 +1,3 @@
module.exports = {
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
semi: false
}

View File

@@ -1,11 +0,0 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin
file_server
}

View File

@@ -1,72 +0,0 @@
#!/usr/local/bin/node
// @ts-check
import { execSync, spawn } from "child_process"
import fs from "fs"
import process from "process"
function runChildProcessWithPrefix(command, args, prefix) {
const childProcess = spawn(command, args);
childProcess.stdout.on('data', (data) => {
const output = data.toString().trim().split('\n');
output.forEach((line) => {
console.log(`${prefix} | ${line}`);
});
});
childProcess.stderr.on('data', (data) => {
const error = data.toString().trim().split('\n');
error.forEach((line) => {
console.error(`${prefix} | ${line}`);
});
});
childProcess.on('close', (code) => {
console.log(`${prefix} Child process exited with code ${code}`);
});
childProcess.on('error', (stuff) => {
console.log("error")
console.log(stuff)
})
return childProcess
}
const envFileContent = Object.entries(process.env)
.filter(([env]) => env.startsWith("VITE_"))
.map(([env, val]) => `${env}=${
(val.startsWith("\"") && val.endsWith("\""))
? val
: `"${val}"`
}`)
.join("\n")
fs.writeFileSync("build.env", envFileContent)
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
fs.rmSync("build.env")
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
caddyProcess.on("exit", (code) => {
console.log(`Exiting process because Caddy Server exited with code ${code}`)
process.exit(code)
})
backendProcess.on("exit", (code) => {
console.log(`Exiting process because Backend Server exited with code ${code}`)
process.exit(code)
})
process.on('SIGINT', () => {
console.log("SIGINT received, exiting...")
caddyProcess.kill("SIGINT")
backendProcess.kill("SIGINT")
process.exit(0)
})

View File

@@ -19,12 +19,10 @@ services:
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
- PORT=3000
volumes:
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
# - ./packages/hoppscotch-backend/:/usr/src/app
- ./packages/hoppscotch-backend/:/usr/src/app
- /usr/src/app/node_modules/
depends_on:
hoppscotch-db:
condition: service_healthy
- hoppscotch-db
ports:
- "3170:3000"
@@ -62,20 +60,12 @@ services:
# you are using an external postgres instance
# This will be exposed at port 5432
hoppscotch-db:
image: postgres:15
image: postgres
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

@@ -17,12 +17,12 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/language": "^6.9.0",
"@lezer/highlight": "^1.1.6",
"@lezer/lr": "^1.3.10"
"@codemirror/language": "^6.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.2.0"
},
"devDependencies": {
"@lezer/generator": "^1.5.0",
"@lezer/generator": "^1.1.0",
"mocha": "^9.2.2",
"rollup": "^2.70.2",
"rollup-plugin-dts": "^4.2.1",

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.4.8",
"version": "2023.4.7",
"description": "",
"author": "",
"private": true,
@@ -33,7 +33,7 @@
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.2.1",
"@nestjs/throttler": "^4.0.0",
"@prisma/client": "^4.16.2",
"@prisma/client": "^4.7.1",
"apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1",
"argon2": "^0.30.3",
@@ -57,7 +57,7 @@
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-microsoft": "^1.0.0",
"prisma": "^4.16.2",
"prisma": "^4.7.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.6.0"

View File

@@ -5,7 +5,7 @@ datasource db {
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"]
binaryTargets = ["native", "debian-openssl-1.1.x"]
}
model Team {

View File

@@ -1,9 +0,0 @@
import { Controller, Get } from '@nestjs/common';
@Controller('ping')
export class AppController {
@Get()
ping(): string {
return 'Success';
}
}

View File

@@ -19,7 +19,6 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
import { ShortcodeModule } from './shortcode/shortcode.module';
import { COOKIES_NOT_FOUND } from './errors';
import { ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app/app.controller';
@Module({
imports: [
@@ -82,6 +81,5 @@ import { AppController } from './app/app.controller';
ShortcodeModule,
],
providers: [GQLComplexityPlugin],
controllers: [AppController],
})
export class AppModule {}

View File

@@ -107,7 +107,7 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
};
/**
* Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable
* Check to see if given auth provider is present in the ALLOWED_AUTH_PROVIDERS env variable
*
* @param provider Provider we want to check the presence of
* @returns Boolean if provider specified is present or not
@@ -117,8 +117,8 @@ export function authProviderCheck(provider: string) {
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
}
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
const envVariables = process.env.ALLOWED_AUTH_PROVIDERS
? process.env.ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
provider.trim().toUpperCase(),
)
: [];

View File

@@ -29,22 +29,22 @@ export const JSON_INVALID = 'json_invalid';
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
* Environment variable "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';
'"ALLOWED_AUTH_PROVIDERS" is not present in .env file';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
* Environment variable "ALLOWED_AUTH_PROVIDERS" is empty in .env file
*/
export const ENV_EMPTY_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
'"ALLOWED_AUTH_PROVIDERS" is empty in .env file';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
* Environment variable "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';
'"ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
/**
* Tried to delete a user data document from fb firestore but failed.

View File

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

View File

@@ -9,12 +9,7 @@ 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 {
ENV_EMPTY_AUTH_PROVIDERS,
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
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';
/**
@@ -161,21 +156,21 @@ export function isValidLength(title: string, length: number) {
/**
* This function is called by bootstrap() in main.ts
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* It checks if the "ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* If not, it throws an error.
*/
export function checkEnvironmentAuthProvider() {
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
if (!process.env.hasOwnProperty('ALLOWED_AUTH_PROVIDERS')) {
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
}
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
if (process.env.ALLOWED_AUTH_PROVIDERS === '') {
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
}
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
',',
).map((provider) => provider.toLocaleUpperCase());
const givenAuthProviders = process.env.ALLOWED_AUTH_PROVIDERS.split(',').map(
(provider) => provider.toLocaleUpperCase(),
);
const supportedAuthProviders = Object.values(AuthProvider).map(
(provider: string) => provider.toLocaleUpperCase(),
);

View File

@@ -29,18 +29,8 @@ module.exports = {
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier": [
"prettier/prettier":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{},
{
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2,
},
],
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",

View File

@@ -1,8 +1,3 @@
module.exports = {
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
semi: false
}

View File

@@ -190,11 +190,10 @@ a {
@apply border-solid border-dividerDark;
@apply rounded;
@apply shadow-lg;
@apply max-w-[45vw] #{!important};
.tippy-content {
@apply flex flex-col;
@apply max-h-[45vh];
@apply max-h-56;
@apply items-stretch;
@apply overflow-y-auto;
@apply text-secondary text-body;
@@ -202,10 +201,6 @@ a {
@apply leading-normal;
@apply focus:outline-none;
scroll-behavior: smooth;
& > span {
@apply block #{!important};
}
}
.tippy-svg-arrow {
@@ -221,7 +216,6 @@ a {
[data-v-tippy] {
@apply flex flex-1;
@apply truncate;
}
[interactive] > div {

View File

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

View File

@@ -31,7 +31,6 @@
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Prettify",
"rename": "Rename",
"remove": "Remove",
"restore": "Restore",
"save": "Save",
@@ -69,8 +68,6 @@
"invite": "Invite",
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
"invite_your_friends": "Invite your friends",
"social_links": "Social links",
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
"join_discord_community": "Join our Discord community",
"keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch",
@@ -135,7 +132,6 @@
"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",
@@ -153,7 +149,6 @@
"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": {
@@ -194,7 +189,6 @@
"schema": "Connect to a GraphQL endpoint to view schema",
"shortcodes": "Shortcodes are empty",
"subscription": "Subscriptions are empty",
"suggestions": "No matching suggestions found",
"team_name": "Team name empty",
"teams": "You don't belong to any teams",
"tests": "There are no tests for this request"
@@ -205,25 +199,18 @@
"create_new": "Create new environment",
"created": "Environment created",
"deleted": "Environment deletion",
"duplicated": "Environment duplicated",
"edit": "Edit Environment",
"global": "Global",
"empty_variables": "No variables",
"global_variables": "Global variables",
"invalid_name": "Please provide a name for the environment",
"list": "Environment variables",
"my_environments": "My Environments",
"name": "Name",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment",
"no_active_environment": "No active environment",
"no_environment": "No environment",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable",
"scope": "Scope",
"select": "Select environment",
"set": "Set environment",
"set_as_environment": "Set as environment",
"team_environments": "Team Environments",
"title": "Environments",
@@ -253,7 +240,6 @@
"no_duration": "No duration",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script"
@@ -310,30 +296,6 @@
"preview": "Hide Preview",
"sidebar": "Collapse sidebar"
},
"inspections": {
"title": "Inspector",
"description": "Inspect possible errors",
"environment": {
"add_environment": "Add to Environment",
"not_found": "Environment variable “{environment}” not found."
},
"header": {
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
},
"response": {
"401_error": "Please check your authentication credentials.",
"404_error": "Please check your request URL and method type.",
"network_error": "Please check your network connection.",
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
"default_error": "Please check your request."
},
"url": {
"extension_not_installed": "Extension not installed.",
"extention_not_enabled": "Extension not enabled.",
"extention_enable_action": "Enable Browser Extension",
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list."
}
},
"import": {
"collections": "Import collections",
"curl": "Import cURL",
@@ -470,7 +432,6 @@
"payload": "Payload",
"query": "Query",
"raw_body": "Raw Request Body",
"rename": "Rename Request",
"renamed": "Request renamed",
"run": "Run",
"save": "Save",
@@ -512,9 +473,9 @@
"account_name_description": "This is your display name.",
"background": "Background",
"black_mode": "Black",
"dark_mode": "Dark",
"change_font_size": "Change font size",
"choose_language": "Choose language",
"dark_mode": "Dark",
"delete_account": "Delete account",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"expand_navigation": "Expand navigation",
@@ -598,9 +559,6 @@
"delete_method": "Select DELETE method",
"get_method": "Select GET method",
"head_method": "Select HEAD method",
"rename": "Rename Current Request",
"import_curl": "Import cURL",
"show_code": "Show generated code",
"method": "Method",
"next_method": "Select Next method",
"post_method": "Select POST method",
@@ -609,7 +567,6 @@
"reset_request": "Reset Request",
"save_to_collections": "Save to Collections",
"send_request": "Send Request",
"save_request": "Save Request",
"title": "Request"
},
"response": {
@@ -618,10 +575,10 @@
"title": "Response"
},
"theme": {
"black": "Switch theme to Black Mode",
"dark": "Switch theme to Dark Mode",
"light": "Switch theme to Light Mode",
"system": "Switch theme to System Mode",
"black": "Switch theme to black mode",
"dark": "Switch theme to dark mode",
"light": "Switch theme to light mode",
"system": "Switch theme to system mode",
"title": "Theme"
}
},
@@ -640,79 +597,8 @@
"url": "URL"
},
"spotlight": {
"general": {
"help_menu": "Open help and support menu",
"chat": "Chat with support",
"open_docs": "Read Documentation",
"open_keybindings": "Open keyboard shortcuts",
"social": "Social links and GitHub",
"title": "General"
},
"miscellaneous": {
"invite": "Invite people to Hoppscotch",
"title": "Miscellaneous"
},
"request": {
"tab_parameters": "Open parameters tab",
"tab_body": "Open body tab",
"tab_headers": "Open headers tab",
"tab_authorization": "Open authorization tab",
"tab_pre_request_script": "Open pre-request script tab",
"tab_tests": "Open tests tab"
},
"response": {
"copy": "Copy response as JSON",
"download": "Download response as file",
"title": "Response"
},
"environments": {
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"edit": "Edit selected environment",
"delete": "Delete selected environment",
"duplicate": "Duplicate selected environment",
"edit_global": "Edit global environment",
"duplicate_global": "Duplicate global environment",
"title": "Environments"
},
"workspace": {
"new": "Create new team",
"edit": "Edit selected team",
"delete": "Delete selected team",
"invite": "Invite people to team",
"switch_to_personal": "Switch to personal workspace",
"title": "Teams"
},
"tab": {
"close_current": "Close current tab",
"close_others": "Close others tab",
"new_tab": "Open a new tab",
"title": "Tabs"
},
"section": {
"user": "User",
"theme": "Theme",
"interface": "Interface",
"interceptor": "Interceptor"
},
"change_interceptor": "Change Interceptor",
"change_language": "Change Language",
"install_extension": "Install Browser Extension",
"settings": {
"theme": {
"black": "Black Mode",
"dark": "Dark Mode",
"light": "Light Mode",
"system": "System Mode"
},
"font": {
"size_sm": "Change to Small",
"size_md": "Change to Medium",
"size_lg": "Change to Large"
},
"change_interceptor": "Change Interceptor",
"change_language": "Change Language",
"install_extension": "Install Browser Extension"
"user": "User"
}
},
"sse": {
@@ -772,11 +658,8 @@
"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": "無法為父集合不同的集合重新排序",
"edit": "編輯合",
"invalid_name": "請提供有效的合名稱",
"invalid_root_move": "集合已在根目錄",
"moved": "移動成功",
"my_collections": "我的合",
"name": "我的新合",
"name_length_insufficient": "合名稱至少要有 3 個字元。",
"new": "建立合",
"order_changed": "集合順序已更新",
"renamed": "合已重新命名",
"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": "合已重新命名",
"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": "您要儲存在此分頁做出的改動嗎?",
"request_change": "您確定要捨棄當前請求嗎?未儲存的變更將遺失。",
"save_unsaved_tab": "Do you want to save changes made in this 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": "您有未儲存的改動",
"collections": "合",
"close_unsaved_tab": "You have unsaved changes",
"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": "無法重新排列來自不同集合的請求",
"duplicated": "已複製請求",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "持續時間",
"enter_curl": "輸入 cURL",
"generate_code": "產生程式碼",
@@ -405,10 +405,10 @@
"header_list": "請求標頭列表",
"invalid_name": "請提供請求名稱",
"method": "方法",
"moved": "已移動請求",
"moved": "Request moved",
"name": "請求名稱",
"new": "新請求",
"order_changed": "已更新請求順序",
"order_changed": "Request Order Updated",
"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": "集合 ID 無效",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "電子信箱格式無效",
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
"invalid_invite_link": "邀請連結無效",
@@ -690,21 +690,21 @@
"member_removed": "使用者已移除",
"member_role_updated": "使用者角色已更新",
"members": "成員",
"more_members": "還有 {count} ",
"more_members": "+{count} more",
"name_length_insufficient": "團隊名稱至少為 6 個字元",
"name_updated": "團隊名稱已更新",
"new": "新團隊",
"new_created": "已建立新團隊",
"new_name": "我的新團隊",
"no_access": "您沒有編輯合的許可權",
"no_access": "您沒有編輯合的許可權",
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
"no_request_found": "找不到請求。",
"no_request_found": "Request not found.",
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
"parent_coll_move": "無法將集合移動至子集合",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "待定邀請",
"permissions": "許可權",
"same_target_destination": "目標和目的地相同",
"same_target_destination": "Same target and destination",
"saved": "團隊已儲存",
"select_a_team": "選擇團隊",
"title": "團隊",
@@ -734,9 +734,9 @@
"url": "網址"
},
"workspace": {
"change": "切換工作區",
"personal": "我的工作區",
"team": "團隊工作區",
"title": "工作區"
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.4.8",
"version": "2023.4.7",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -22,140 +22,137 @@
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.9.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.9",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.9.0",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.0",
"@codemirror/search": "^6.5.1",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.16.0",
"@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.9",
"@codemirror/autocomplete": "^6.0.3",
"@codemirror/commands": "^6.0.1",
"@codemirror/lang-javascript": "^6.0.1",
"@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/language": "^6.2.0",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.1.0",
"@codemirror/view": "^6.0.2",
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@hoppscotch/ui": "workspace:^",
"@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "^1.1.6",
"@sentry/tracing": "^7.64.0",
"@sentry/vue": "^7.64.0",
"@urql/core": "^4.1.1",
"@lezer/highlight": "^1.0.0",
"@sentry/tracing": "^7.13.0",
"@sentry/vue": "^7.13.0",
"@urql/core": "^2.5.0",
"@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6",
"@urql/exchange-graphcache": "^6.3.2",
"@vitejs/plugin-legacy": "^4.1.1",
"@vueuse/core": "^10.3.0",
"@vueuse/head": "^1.3.1",
"@urql/exchange-auth": "^0.1.7",
"@urql/exchange-graphcache": "^4.4.3",
"@vitejs/plugin-legacy": "^2.3.0",
"@vueuse/core": "^8.9.4",
"@vueuse/head": "^0.7.9",
"acorn-walk": "^8.2.0",
"axios": "^1.4.0",
"axios": "^0.21.4",
"buffer": "^6.0.3",
"dioc": "workspace:^",
"esprima": "^4.0.1",
"events": "^3.3.0",
"fp-ts": "^2.16.1",
"fp-ts": "^2.12.1",
"fuse.js": "^6.6.2",
"globalthis": "^1.0.3",
"graphql": "^16.8.0",
"graphql": "^15.5.0",
"graphql-language-service-interface": "^2.9.1",
"graphql-tag": "^2.12.6",
"httpsnippet": "^2.0.0",
"insomnia-importers": "^3.6.0",
"io-ts": "^2.2.20",
"insomnia-importers": "^3.3.0",
"io-ts": "^2.2.16",
"js-yaml": "^4.1.0",
"jsonpath-plus": "^7.2.0",
"jsonpath-plus": "^7.0.0",
"lodash-es": "^4.17.21",
"lossless-json": "^2.0.11",
"lossless-json": "^2.0.8",
"minisearch": "^6.1.0",
"nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0",
"path": "^0.12.7",
"postman-collection": "^4.2.0",
"postman-collection": "^4.1.4",
"process": "^0.11.10",
"qs": "^6.11.2",
"rxjs": "^7.8.1",
"qs": "^6.10.3",
"rxjs": "^7.5.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
"socketio-wildcard": "^2.0.0",
"splitpanes": "^3.1.5",
"splitpanes": "^3.1.1",
"stream-browserify": "^3.0.0",
"subscriptions-transport-ws": "^0.11.0",
"tern": "^0.24.3",
"timers": "^0.1.1",
"tippy.js": "^6.3.7",
"url": "^0.11.1",
"util": "^0.12.5",
"uuid": "^9.0.0",
"vue": "^3.3.4",
"url": "^0.11.0",
"util": "^0.12.4",
"uuid": "^8.3.2",
"vue": "^3.2.25",
"vue-i18n": "^9.2.2",
"vue-pdf-embed": "^1.1.6",
"vue-router": "^4.2.4",
"vue-tippy": "6.3.1",
"vue-pdf-embed": "^1.1.4",
"vue-router": "^4.0.16",
"vue-tippy": "6.0.0-alpha.58",
"vuedraggable-es": "^4.1.1",
"wonka": "^6.3.4",
"workbox-window": "^7.0.0",
"xml-formatter": "^3.5.0",
"wonka": "^4.0.15",
"workbox-window": "^6.5.4",
"xml-formatter": "^3.4.1",
"yargs-parser": "^21.1.1"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
"@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/lucide": "^1.1.119",
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
"@graphql-codegen/add": "^3.2.0",
"@graphql-codegen/cli": "^2.8.0",
"@graphql-codegen/typed-document-node": "^2.3.1",
"@graphql-codegen/typescript": "^2.7.1",
"@graphql-codegen/typescript-operations": "^2.5.1",
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
"@graphql-codegen/urql-introspection": "^2.2.0",
"@graphql-typed-document-node/core": "^3.1.1",
"@iconify-json/lucide": "^1.1.109",
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.3.3",
"@rushstack/eslint-patch": "^1.1.4",
"@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.8",
"@types/lodash-es": "^4.17.6",
"@types/lossless-json": "^1.0.1",
"@types/nprogress": "^0.2.0",
"@types/paho-mqtt": "^1.0.7",
"@types/paho-mqtt": "^1.0.6",
"@types/postman-collection": "^3.5.7",
"@types/splitpanes": "^2.2.1",
"@types/uuid": "^9.0.2",
"@types/uuid": "^8.3.4",
"@types/yargs-parser": "^21.0.0",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-core": "^3.3.4",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"@vitejs/plugin-vue": "^3.1.0",
"@vue/compiler-sfc": "^3.2.39",
"@vue/eslint-config-typescript": "^11.0.1",
"@vue/runtime-core": "^3.2.39",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"eslint": "^8.47.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"dotenv": "^16.0.3",
"eslint": "^8.24.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.5.1",
"npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3",
"rollup-plugin-polyfill-node": "^0.12.0",
"sass": "^1.66.0",
"typescript": "^5.1.6",
"unplugin-fonts": "^1.0.3",
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.9",
"vite-plugin-checker": "^0.6.1",
"vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.38",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-vue-layouts": "^0.8.0",
"vite-plugin-windicss": "^1.9.1",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8",
"openapi-types": "^12.0.0",
"rollup-plugin-polyfill-node": "^0.10.1",
"sass": "^1.53.0",
"typescript": "^4.5.4",
"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",
"vite-plugin-pages-sitemap": "^1.4.5",
"vite-plugin-pwa": "^0.13.1",
"vite-plugin-vue-layouts": "^0.7.0",
"vite-plugin-windicss": "^1.8.8",
"vitest": "^0.32.2",
"vue-tsc": "^0.38.2",
"windicss": "^3.5.6"
}
}

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 389 B

View File

@@ -14,7 +14,6 @@ declare module '@vue/runtime-core' {
AppFooter: typeof import('./components/app/Footer.vue')['default']
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default']
AppInspection: typeof import('./components/app/Inspection.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']
@@ -24,13 +23,10 @@ declare module '@vue/runtime-core' {
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSocial: typeof import('./components/app/Social.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']
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.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']
@@ -89,21 +85,17 @@ declare module '@vue/runtime-core' {
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']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
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']
@@ -125,18 +117,14 @@ 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']
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
HttpTests: typeof import('./components/http/Tests.vue')['default']
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -149,7 +137,6 @@ declare module '@vue/runtime-core' {
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']
@@ -169,8 +156,6 @@ declare module '@vue/runtime-core' {
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: typeof import('./components/settings/Proxy.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']
@@ -182,7 +167,6 @@ declare module '@vue/runtime-core' {
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']
@@ -197,8 +181,8 @@ declare module '@vue/runtime-core' {
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('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
SmartTreeBranch: typeof import('./../../hoppscotch-ui/src/components/smart/TreeBranch.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']

View File

@@ -1,57 +1,17 @@
<template>
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
<AppShare :show="showShare" @hide-modal="showShare = false" />
<AppSocial :show="showSocial" @hide-modal="showSocial = false" />
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@hide-modal="confirmRemove = false"
@resolve="deleteTeam()"
/>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { showChat } from "~/modules/crisp"
import { useToast } from "~/composables/toast"
import { useI18n } from "~/composables/i18n"
const toast = useToast()
const t = useI18n()
import { defineActionHandler } from "~/helpers/actions"
const showShortcuts = ref(false)
const showShare = ref(false)
const showSocial = ref(false)
const showLogin = ref(false)
const confirmRemove = ref(false)
const teamID = ref<string | null>(null)
const deleteTeam = () => {
if (!teamID.value) return
pipe(
backendDeleteTeam(teamID.value),
TE.match(
(err) => {
// TODO: Better errors ? We know the possible errors now
toast.error(`${t("error.something_went_wrong")}`)
console.error(err)
},
() => {
invokeAction("workspace.switch.personal")
toast.success(`${t("team.deleted")}`)
}
)
)() // Tasks (and TEs) are lazy, so call the function returned
}
defineActionHandler("flyouts.keybinds.toggle", () => {
showShortcuts.value = !showShortcuts.value
})
@@ -60,20 +20,7 @@ defineActionHandler("modals.share.toggle", () => {
showShare.value = !showShare.value
})
defineActionHandler("modals.social.toggle", () => {
showSocial.value = !showSocial.value
})
defineActionHandler("modals.login.toggle", () => {
showLogin.value = !showLogin.value
})
defineActionHandler("flyouts.chat.open", () => {
showChat()
})
defineActionHandler("modals.team.delete", ({ teamId }) => {
teamID.value = teamId
confirmRemove.value = true
})
</script>

View File

@@ -18,14 +18,14 @@
</div>
<div class="inline-flex items-center justify-center flex-1 space-x-2">
<button
class="flex flex-1 items-center justify-between px-2 py-1 self-stretch bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-60 hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
class="flex flex-1 items-center justify-between px-2 py-1 bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-xs hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
@click="invokeAction('modals.search.toggle')"
>
<span class="inline-flex flex-1 items-center">
<icon-lucide-search class="mr-2 svg-icons" />
{{ t("app.search") }}
</span>
<span class="flex space-x-1">
<span class="flex">
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
<kbd class="shortcut-key">K</kbd>
</span>
@@ -382,22 +382,6 @@ const settings = ref<any | null>(null)
const logout = ref<any | null>(null)
const accountActions = ref<any | null>(null)
defineActionHandler("modals.team.edit", () => {
// TODO: Remove this hack
setTimeout(() => {
handleTeamEdit()
}, 100)
})
defineActionHandler("modals.team.invite", () => {
if (
selectedTeam.value?.myRole === "OWNER" ||
selectedTeam.value?.myRole === "EDITOR"
) {
inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
}
})
defineActionHandler(
"user.login",
() => {

View File

@@ -1,112 +0,0 @@
<template>
<div v-if="inspectionResults && inspectionResults.length > 0">
<tippy interactive trigger="click" theme="popover">
<div class="flex justify-center items-center flex-1 flex-col">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconAlertTriangle"
:class="severityColor(getHighestSeverity.severity)"
:title="t('inspections.description')"
/>
</div>
<template #content="{ hide }">
<div class="flex flex-col space-y-2 items-start flex-1">
<div
class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
>
<span class="flex items-center flex-1">
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
<span class="font-bold">
{{ t("inspections.title") }}
</span>
</span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/inspections"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
</div>
<div
v-for="(inspector, index) in inspectionResults"
:key="index"
class="flex self-stretch max-w-md w-full"
>
<div
class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
>
<span
v-if="inspector.text.type === 'text'"
class="flex-1 px-3 py-2"
>
{{ inspector.text.text }}
<HoppSmartLink
blank
:to="inspector.doc.link"
class="text-accent hover:text-accentDark transition"
>
{{ inspector.doc.text }}
<icon-lucide-arrow-up-right class="svg-icons" />
</HoppSmartLink>
</span>
<span v-if="inspector.action" class="flex p-2 space-x-2">
<HoppButtonSecondary
:label="inspector.action.text"
outline
filled
@click="
() => {
inspector.action?.apply()
hide()
}
"
/>
</span>
</div>
</div>
</div>
</template>
</tippy>
</div>
</template>
<script lang="ts" setup>
import { InspectorResult } from "~/services/inspection"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import IconHelpCircle from "~icons/lucide/help-circle"
import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
const props = defineProps<{
inspectionResults: InspectorResult[] | undefined
}>()
const getHighestSeverity = computed(() => {
if (props.inspectionResults) {
return props.inspectionResults.reduce(
(prev, curr) => {
return prev.severity > curr.severity ? prev : curr
},
{ severity: 0 }
)
} else {
return { severity: 0 }
}
})
const severityColor = (severity: number) => {
switch (severity) {
case 1:
return "!text-green-500 hover:!text-green-600"
case 2:
return "!text-yellow-500 hover:!text-yellow-600"
case 3:
return "!text-red-500 hover:!text-red-600"
default:
return "!text-gray-500 hover:!text-gray-600"
}
}
</script>

View File

@@ -8,41 +8,91 @@
{{ t("settings.interceptor_description") }}
</p>
</div>
<div>
<div
v-for="interceptor in interceptors"
:key="interceptor.interceptorID"
class="flex flex-col"
>
<HoppSmartRadio
:value="interceptor.interceptorID"
:label="unref(interceptor.name(t))"
:selected="interceptorSelection === interceptor.interceptorID"
@change="interceptorSelection = interceptor.interceptorID"
/>
<component
:is="interceptor.selectorSubtitle"
v-if="interceptor.selectorSubtitle"
/>
</div>
<HoppSmartRadioGroup
v-model="interceptorSelection"
:radios="interceptors"
/>
<div
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
class="flex space-x-2"
>
<HoppButtonSecondary
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
:icon="IconChrome"
label="Chrome"
outline
class="!flex-1"
/>
<HoppButtonSecondary
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
:icon="IconFirefox"
label="Firefox"
outline
class="!flex-1"
/>
</div>
</div>
</template>
<script setup lang="ts">
import IconChrome from "~icons/brands/chrome"
import IconFirefox from "~icons/brands/firefox"
import { computed } from "vue"
import { applySetting, toggleSetting } from "~/newstore/settings"
import { useSetting } from "@composables/settings"
import { useI18n } from "@composables/i18n"
import { useService } from "dioc/vue"
import { Ref, unref } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
import { useReadonlyStream } from "@composables/stream"
import { extensionStatus$ } from "~/newstore/HoppExtension"
const t = useI18n()
const interceptorService = useService(InterceptorService)
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
const interceptorSelection =
interceptorService.currentInterceptorID as Ref<string>
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
const interceptors = interceptorService.availableInterceptors
const extensionVersion = computed(() => {
return currentExtensionStatus.value === "available"
? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null
: null
})
const interceptors = computed(() => [
{ value: "BROWSER_ENABLED" as const, label: t("state.none") },
{ value: "PROXY_ENABLED" as const, label: t("settings.proxy") },
{
value: "EXTENSIONS_ENABLED" as const,
label:
`${t("settings.extensions")}: ` +
(extensionVersion.value !== null
? `v${extensionVersion.value.major}.${extensionVersion.value.minor}`
: t("settings.extension_ver_not_reported")),
},
])
type InterceptorMode = (typeof interceptors)["value"][number]["value"]
const interceptorSelection = computed<InterceptorMode>({
get() {
if (PROXY_ENABLED.value) return "PROXY_ENABLED"
if (EXTENSIONS_ENABLED.value) return "EXTENSIONS_ENABLED"
return "BROWSER_ENABLED"
},
set(val) {
if (val === "EXTENSIONS_ENABLED") {
applySetting("EXTENSIONS_ENABLED", true)
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
}
if (val === "PROXY_ENABLED") {
applySetting("PROXY_ENABLED", true)
if (EXTENSIONS_ENABLED.value) toggleSetting("EXTENSIONS_ENABLED")
}
if (val === "BROWSER_ENABLED") {
applySetting("PROXY_ENABLED", false)
applySetting("EXTENSIONS_ENABLED", false)
}
},
})
</script>

View File

@@ -130,12 +130,13 @@
@click="nativeShare()"
/>
</div>
<AppShare :show="showShare" @hide-modal="showShare = false" />
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { watch } from "vue"
import { ref, watch } from "vue"
import IconSidebar from "~icons/lucide/sidebar"
import IconSidebarOpen from "~icons/lucide/sidebar-open"
import IconBook from "~icons/lucide/book"
@@ -150,12 +151,13 @@ import IconUserPlus from "~icons/lucide/user-plus"
import IconShare2 from "~icons/lucide/share-2"
import IconChevronRight from "~icons/lucide/chevron-right"
import { useSetting } from "@composables/settings"
import { invokeAction } from "~/helpers/actions"
import { defineActionHandler } from "~/helpers/actions"
import { showChat } from "@modules/crisp"
import { useI18n } from "@composables/i18n"
const t = useI18n()
const navigatorShare = !!navigator.share
const showShare = ref(false)
const ZEN_MODE = useSetting("ZEN_MODE")
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
@@ -172,6 +174,10 @@ defineProps<{
show: boolean
}>()
defineActionHandler("modals.share.toggle", () => {
showShare.value = !showShare.value
})
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
@@ -192,7 +198,7 @@ const expandCollection = () => {
}
const expandInvite = () => {
invokeAction("modals.share.toggle")
showShare.value = true
}
const nativeShare = () => {

View File

@@ -4,13 +4,15 @@
<div
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
>
<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 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>
</div>
<div class="flex flex-col divide-y divide-dividerLight">
<HoppSmartPlaceholder
@@ -19,7 +21,6 @@
>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</HoppSmartPlaceholder>
<details
v-for="(sectionResults, sectionTitle) in shortcutsResults"
v-else

View File

@@ -1,135 +0,0 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('app.social_links')"
@close="hideModal"
>
<template #body>
<div class="flex flex-col space-y-2">
<div class="grid grid-cols-3 gap-4">
<a
v-for="(platform, index) in platforms"
:key="`platform-${index}`"
:href="platform.link"
target="_blank"
class="social-link"
tabindex="0"
>
<component :is="platform.icon" class="w-6 h-6" />
<span class="mt-3">
{{ platform.name }}
</span>
</a>
<button class="social-link" @click="copyAppLink">
<component :is="copyIcon" class="w-6 h-6 text-xl" />
<span class="mt-3">
{{ t("app.copy") }}
</span>
</button>
</div>
</div>
</template>
<template #footer>
<p class="text-secondaryLight">
{{ t("app.social_description") }}
</p>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import IconFacebook from "~icons/brands/facebook"
import IconLinkedIn from "~icons/brands/linkedin"
import IconReddit from "~icons/brands/reddit"
import IconTwitter from "~icons/brands/twitter"
import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy"
import IconGitHub from "~icons/lucide/github"
const t = useI18n()
const toast = useToast()
defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const url = "https://hoppscotch.io"
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const platforms = [
{
name: "GitHub",
icon: IconGitHub,
link: `https://hoppscotch.io/github`,
},
{
name: "Twitter",
icon: IconTwitter,
link: `https://twitter.com/hoppscotch_io`,
},
{
name: "Facebook",
icon: IconFacebook,
link: `https://www.facebook.com/hoppscotch.io`,
},
{
name: "Reddit",
icon: IconReddit,
link: `https://www.reddit.com/r/hoppscotch`,
},
{
name: "LinkedIn",
icon: IconLinkedIn,
link: `https://www.linkedin.com/company/hoppscotch/`,
},
]
const copyAppLink = () => {
copyToClipboard(url)
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const hideModal = () => {
emit("hide-modal")
}
</script>
<style lang="scss" scoped>
.social-link {
@apply border border-dividerLight;
@apply rounded;
@apply flex-col flex;
@apply p-4;
@apply items-center;
@apply justify-center;
@apply font-semibold;
@apply hover: (bg-primaryLight text-secondaryDark);
@apply focus: outline-none;
@apply focus-visible: border-divider;
svg {
@apply opacity-80;
}
&:hover {
svg {
@apply opacity-100;
}
}
}
</style>

View File

@@ -80,11 +80,10 @@ const props = defineProps<{
active: boolean
}>()
const formattedShortcutKeys = computed(
() =>
props.entry.meta?.keyboardShortcut?.map((key) => {
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
})
const formattedShortcutKeys = computed(() =>
props.entry.meta?.keyboardShortcut?.map((key) => {
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
})
)
const emit = defineEmits<{

View File

@@ -1,65 +0,0 @@
<template>
<span class="flex flex-1 space-x-2 items-center">
<template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { graphqlCollectionStore } from "~/newstore/collections"
const props = defineProps<{
folderPath: string
}>()
const pathFolders = computed(() => {
try {
const folderIndicies = props.folderPath
.split("/")
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection<HoppGQLRequest>[] = []
let currentFolder =
graphqlCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
pathItems.push(folder)
currentFolder = folder
}
return pathItems
} catch (e) {
console.error(e)
return []
}
})
const request = computed(() => {
try {
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex
]
} catch (e) {
return null
}
})
</script>

View File

@@ -1,71 +0,0 @@
<template>
<span class="flex flex-1 items-center space-x-2">
<template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span
v-if="request"
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
:class="getMethodLabelColorClassOf(request)"
>
{{ request.method.toUpperCase() }}
</span>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { restCollectionStore } from "~/newstore/collections"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
const props = defineProps<{
folderPath: string
}>()
const pathFolders = computed(() => {
try {
const folderIndicies = props.folderPath
.split("/")
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection<HoppRESTRequest>[] = []
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
pathItems.push(folder)
currentFolder = folder
}
return pathItems
} catch (e) {
console.error(e)
return []
}
})
const request = computed(() => {
try {
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex
]
} catch (e) {
return null
}
})
</script>

View File

@@ -95,22 +95,6 @@ import {
import { isEqual } from "lodash-es"
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
import {
EnvironmentsSpotlightSearcherService,
SwitchEnvSpotlightSearcherService,
} from "~/services/spotlight/searchers/environment.searcher"
import {
SwitchWorkspaceSpotlightSearcherService,
WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher"
const t = useI18n()
@@ -126,18 +110,6 @@ const spotlightService = useService(SpotlightService)
useService(HistorySpotlightSearcherService)
useService(UserSpotlightSearcherService)
useService(NavigationSpotlightSearcherService)
useService(SettingsSpotlightSearcherService)
useService(CollectionsSpotlightSearcherService)
useService(MiscellaneousSpotlightSearcherService)
useService(TabSpotlightSearcherService)
useService(GeneralSpotlightSearcherService)
useService(ResponseSpotlightSearcherService)
useService(RequestSpotlightSearcherService)
useService(EnvironmentsSpotlightSearcherService)
useService(SwitchEnvSpotlightSearcherService)
useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
const search = ref("")
@@ -264,4 +236,3 @@ function newUseArrowKeysForNavigation() {
return { selectedEntry }
}
</script>
~/services/spotlight/searchers/workspace.searcher

View File

@@ -6,13 +6,21 @@
@close="hideModal"
>
<template #body>
<HoppSmartInput
v-model="editingName"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="addNewCollection"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -57,28 +65,28 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const editingName = ref("")
const name = ref("")
watch(
() => props.show,
(show) => {
if (!show) {
editingName.value = ""
name.value = ""
}
}
)
const addNewCollection = () => {
if (!editingName.value) {
if (!name.value) {
toast.error(t("collection.invalid_name"))
return
}
emit("submit", editingName.value)
emit("submit", name.value)
}
const hideModal = () => {
editingName.value = ""
name.value = ""
emit("hide-modal")
}
</script>

View File

@@ -6,13 +6,21 @@
@close="emit('hide-modal')"
>
<template #body>
<HoppSmartInput
v-model="editingName"
placeholder=" "
input-styles="floating-input"
:label="t('action.label')"
@submit="addFolder"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -57,27 +65,27 @@ const emit = defineEmits<{
(e: "add-folder", name: string): void
}>()
const editingName = ref("")
const name = ref("")
watch(
() => props.show,
(show) => {
if (!show) {
editingName.value = ""
name.value = ""
}
}
)
const addFolder = () => {
if (editingName.value.trim() === "") {
if (name.value.trim() === "") {
toast.error(t("folder.invalid_name"))
return
}
emit("add-folder", editingName.value)
emit("add-folder", name.value)
}
const hideModal = () => {
editingName.value = ""
name.value = ""
emit("hide-modal")
}
</script>

View File

@@ -6,13 +6,19 @@
@close="$emit('hide-modal')"
>
<template #body>
<HoppSmartInput
v-model="editingName"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="addRequest"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -58,23 +64,23 @@ const emit = defineEmits<{
(event: "add-request", name: string): void
}>()
const editingName = ref("")
const name = ref("")
watch(
() => props.show,
(show) => {
if (show) {
editingName.value = currentActiveTab.value.document.request.name
name.value = currentActiveTab.value.document.request.name
}
}
)
const addRequest = () => {
if (editingName.value.trim() === "") {
if (name.value.trim() === "") {
toast.error(`${t("error.empty_req_name")}`)
return
}
emit("add-request", editingName.value)
emit("add-request", name.value)
}
const hideModal = () => {

View File

@@ -6,13 +6,21 @@
@close="hideModal"
>
<template #body>
<HoppSmartInput
v-model="editingName"
placeholder=" "
input-styles="floating-input"
:label="t('action.label')"
@submit="saveCollection"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -59,26 +67,26 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const editingName = ref("")
const name = ref("")
watch(
() => props.editingCollectionName,
(newName) => {
editingName.value = newName
name.value = newName
}
)
const saveCollection = () => {
if (editingName.value.trim() === "") {
if (name.value.trim() === "") {
toast.error(t("collection.invalid_name"))
return
}
emit("submit", editingName.value)
emit("submit", name.value)
}
const hideModal = () => {
editingName.value = ""
name.value = ""
emit("hide-modal")
}
</script>

View File

@@ -6,13 +6,21 @@
@close="emit('hide-modal')"
>
<template #body>
<HoppSmartInput
v-model="editingName"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="editFolder"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -59,26 +67,26 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const editingName = ref("")
const name = ref("")
watch(
() => props.editingFolderName,
(newName) => {
editingName.value = newName
name.value = newName
}
)
const editFolder = () => {
if (editingName.value.trim() === "") {
if (name.value.trim() === "") {
toast.error(t("folder.invalid_name"))
return
}
emit("submit", editingName.value)
emit("submit", name.value)
}
const hideModal = () => {
editingName.value = ""
name.value = ""
emit("hide-modal")
}
</script>

View File

@@ -6,13 +6,21 @@
@close="hideModal"
>
<template #body>
<HoppSmartInput
v-model="editingName"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="editRequest"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -60,19 +68,19 @@ const emit = defineEmits<{
(e: "update:modelValue", value: string): void
}>()
const editingName = useVModel(props, "modelValue")
const name = useVModel(props, "modelValue")
const editRequest = () => {
if (editingName.value.trim() === "") {
if (name.value.trim() === "") {
toast.error(t("request.invalid_name"))
return
}
emit("submit", editingName.value)
emit("submit", name.value)
}
const hideModal = () => {
editingName.value = ""
name.value = ""
emit("hide-modal")
}
</script>

View File

@@ -32,7 +32,7 @@
</span>
</div>
<div class="flex flex-col flex-1">
<HoppSmartTree :adapter="myAdapter">
<SmartTree :adapter="myAdapter">
<template
#content="{ node, toggleChildren, isOpen, highlightChildren }"
>
@@ -291,7 +291,7 @@
>
</HoppSmartPlaceholder>
</template>
</HoppSmartTree>
</SmartTree>
</div>
</div>
</template>
@@ -303,10 +303,7 @@ import IconHelpCircle from "~icons/lucide/help-circle"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import {
ChildrenResult,
SmartTreeAdapter,
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { pipe } from "fp-ts/function"

View File

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

View File

@@ -46,7 +46,7 @@
</span>
</div>
<div class="flex flex-col overflow-hidden">
<HoppSmartTree :adapter="teamAdapter">
<SmartTree :adapter="teamAdapter">
<template
#content="{ node, toggleChildren, isOpen, highlightChildren }"
>
@@ -311,7 +311,7 @@
</HoppSmartPlaceholder>
</div>
</template>
</HoppSmartTree>
</SmartTree>
</div>
</div>
</template>
@@ -326,10 +326,7 @@ import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { TeamRequest } from "~/helpers/teams/TeamRequest"
import {
ChildrenResult,
SmartTreeAdapter,
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
import { cloneDeep } from "lodash-es"
import { HoppRESTRequest } from "@hoppscotch/data"
import { pipe } from "fp-ts/function"

View File

@@ -6,13 +6,21 @@
@close="hideModal"
>
<template #body>
<HoppSmartInput
v-model="name"
placeholder=" "
input-styles="floating-input"
:label="t('action.label')"
@submit="addNewCollection"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,13 +6,21 @@
@close="$emit('hide-modal')"
>
<template #body>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="addFolder"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,13 +6,21 @@
@close="emit('hide-modal')"
>
<template #body>
<HoppSmartInput
v-model="editingName"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="addRequest"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -57,24 +65,24 @@ const emit = defineEmits<{
): void
}>()
const editingName = ref("")
const name = ref("")
watch(
() => props.show,
(show) => {
if (show) {
editingName.value = getGQLSession().request.name
name.value = getGQLSession().request.name
}
}
)
const addRequest = () => {
if (!editingName.value) {
if (!name.value) {
toast.error(`${t("error.empty_req_name")}`)
return
}
emit("add-request", {
name: editingName.value,
name: name.value,
path: props.folderPath,
})
hideModal()

View File

@@ -6,13 +6,21 @@
@close="hideModal"
>
<template #body>
<HoppSmartInput
v-model="editingName"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="saveCollection"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -52,17 +60,17 @@ const emit = defineEmits<{
const t = useI18n()
const toast = useToast()
const editingName = ref<string | null>()
const name = ref<string | null>()
watch(
() => props.editingCollectionName,
(val) => {
editingName.value = val
name.value = val
}
)
const saveCollection = () => {
if (!editingName.value) {
if (!name.value) {
toast.error(`${t("collection.invalid_name")}`)
return
}
@@ -70,7 +78,7 @@ const saveCollection = () => {
// TODO: Better typechecking here ?
const collectionUpdated = {
...(props.editingCollection as any),
name: editingName.value,
name: name.value,
}
editGraphqlCollection(props.editingCollectionIndex, collectionUpdated)
@@ -78,7 +86,7 @@ const saveCollection = () => {
}
const hideModal = () => {
editingName.value = null
name.value = null
emit("hide-modal")
}
</script>

View File

@@ -6,13 +6,21 @@
@close="$emit('hide-modal')"
>
<template #body>
<HoppSmartInput
v-model="name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="editFolder"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">

View File

@@ -6,13 +6,21 @@
@close="hideModal"
>
<template #body>
<HoppSmartInput
v-model="requestUpdateData.name"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="saveRequest"
/>
<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>
</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 !border-0"
class="py-2 pl-4 pr-2 bg-transparent"
/>
<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
v-model="filterTexts"
:placeholder="t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent !border-0"
type="search"
autocomplete="off"
:placeholder="t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent"
:disabled="collectionsType.type === 'team-collections'"
/>
</div>

View File

@@ -11,7 +11,7 @@
t("environment.name")
}}</label>
<input
v-model="editingName"
v-model="name"
type="text"
:placeholder="t('environment.variable')"
class="input"
@@ -88,6 +88,7 @@ const props = defineProps<{
position: { top: number; left: number }
name: string
value: string
replaceWithVariable: boolean
}>()
const emit = defineEmits<{
@@ -105,7 +106,7 @@ watch(
scope.value = {
type: "global",
}
editingName.value = ""
name.value = ""
replaceWithVariable.value = false
}
}
@@ -131,22 +132,22 @@ const scope = ref<Scope>({
const replaceWithVariable = ref(false)
const editingName = ref(props.name)
const name = ref("")
const addEnvironment = async () => {
if (!editingName.value) {
if (!name.value) {
toast.error(`${t("environment.invalid_name")}`)
return
}
if (scope.value.type === "global") {
addGlobalEnvVariable({
key: editingName.value,
key: name.value,
value: props.value,
})
toast.success(`${t("environment.updated")}`)
} else if (scope.value.type === "my-environment") {
addEnvironmentVariable(scope.value.index, {
key: editingName.value,
key: name.value,
value: props.value,
})
toast.success(`${t("environment.updated")}`)
@@ -154,7 +155,7 @@ const addEnvironment = async () => {
const newVariables = [
...scope.value.environment.environment.variables,
{
key: editingName.value,
key: name.value,
value: props.value,
},
]
@@ -178,7 +179,7 @@ const addEnvironment = async () => {
}
if (replaceWithVariable.value) {
//replace the current tab endpoint with the variable name with << and >>
const variableName = `<<${editingName.value}>>`
const variableName = `<<${name.value}>>`
//replace the currenttab endpoint containing the value in the text with variablename
currentActiveTab.value.document.request.endpoint =
currentActiveTab.value.document.request.endpoint.replace(

View File

@@ -1,305 +1,203 @@
<template>
<div class="flex divide-x divide-dividerLight">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => envSelectorActions!.focus()"
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="bg-transparent select-wrapper"
>
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="select-wrapper"
>
<HoppButtonSecondary
:icon="IconLayers"
:label="
mdAndLarger
? selectedEnv.type !== 'NO_ENV_SELECTED'
? selectedEnv.name
: `${t('environment.select')}`
: ''
"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="envSelectorActions"
role="menu"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:label="`${t('environment.no_environment')}`"
:info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
? IconCheck
: undefined
"
:active-info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
"
@click="
() => {
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
hide()
}
"
/>
<HoppSmartTabs
v-model="selectedEnvTab"
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
!isTeamSelected || workspace.type === 'personal'
? 'bg-primaryLight'
: ''
}`"
render-inactive-tabs
>
<HoppSmartTab
:id="'my-environments'"
:label="`${t('environment.my_environments')}`"
>
<HoppSmartItem
v-for="(gen, index) in myEnvironments"
:key="`gen-${index}`"
:icon="IconLayers"
:label="gen.name"
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
:active-info-icon="index === selectedEnv.index"
@click="
() => {
selectedEnvironmentIndex = {
type: 'MY_ENV',
index: index,
}
hide()
}
"
/>
<div
v-if="myEnvironments.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
>
<div
v-if="teamListLoading"
class="flex flex-col items-center justify-center p-4"
>
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">
{{ t("state.loading") }}
</span>
</div>
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem
v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name"
:info-icon="
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
@click="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide()
}
"
/>
<div
v-if="teamEnvironmentList.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</div>
<div
v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div>
</template>
</tippy>
<span class="flex">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => envQuickPeekActions!.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.quick_peek')}`"
:icon="IconEye"
class="!px-4"
/>
<template #content="{ hide }">
<div
ref="envQuickPeekActions"
role="menu"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<div
class="sticky top-0 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
>
{{ t("environment.global_variables") }}
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.edit')"
:icon="IconEdit"
@click="
() => {
editGlobalEnv()
hide()
}
"
/>
</div>
<div class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4">
<span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span class="w-full min-w-32 truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
<div
v-for="(variable, index) in globalEnvs"
:key="index"
class="flex flex-1 space-x-4"
>
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
{{ variable.key }}
</span>
<span class="text-secondaryLight w-full min-w-32 truncate">
{{ variable.value }}
</span>
</div>
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
{{ t("environment.empty_variables") }}
</div>
</div>
<div
class="sticky top-0 mt-2 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
:class="{
'bg-primaryLight': !selectedEnv.variables,
}"
>
{{ t("environment.list") }}
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:disabled="!selectedEnv.variables"
:title="t('action.edit')"
:icon="IconEdit"
@click="
() => {
editEnv()
hide()
}
"
/>
</div>
<div
v-if="selectedEnv.type === 'NO_ENV_SELECTED'"
class="text-secondaryLight my-2 flex flex-col flex-1 pl-4"
>
{{ t("environment.no_active_environment") }}
</div>
<div v-else class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4">
<span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span class="w-full min-w-32 truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
<div
v-for="(variable, index) in environmentVariables"
:key="index"
class="flex flex-1 space-x-4"
>
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
{{ variable.key }}
</span>
<span class="text-secondaryLight w-full min-w-32 truncate">
{{ variable.value }}
</span>
</div>
<div
v-if="environmentVariables.length === 0"
class="text-secondaryLight"
>
{{ t("environment.empty_variables") }}
</div>
</div>
</div>
</template>
</tippy>
<HoppButtonSecondary
:icon="IconLayers"
:label="
mdAndLarger
? selectedEnv.type !== 'NO_ENV_SELECTED'
? selectedEnv.name
: `${t('environment.select')}`
: ''
"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
</div>
<template #content="{ hide }">
<div
ref="tippyActions"
role="menu"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
v-if="!isScopeSelector"
:label="`${t('environment.no_environment')}`"
:info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
? IconCheck
: undefined
"
:active-info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
"
@click="
() => {
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
hide()
}
"
/>
<HoppSmartItem
v-else-if="isScopeSelector && modelValue"
:label="t('environment.global')"
:icon="IconGlobe"
:info-icon="modelValue.type === 'global' ? IconCheck : undefined"
:active-info-icon="modelValue.type === 'global'"
@click="
() => {
$emit('update:modelValue', {
type: 'global',
})
hide()
}
"
/>
<HoppSmartTabs
v-model="selectedEnvTab"
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary"
render-inactive-tabs
>
<HoppSmartTab
:id="'my-environments'"
:label="`${t('environment.my_environments')}`"
>
<HoppSmartItem
v-for="(gen, index) in myEnvironments"
:key="`gen-${index}`"
:icon="IconLayers"
:label="gen.name"
:info-icon="isEnvActive(index) ? IconCheck : undefined"
:active-info-icon="isEnvActive(index)"
@click="
() => {
handleEnvironmentChange(index, {
type: 'my-environment',
environment: gen,
})
hide()
}
"
/>
<HoppSmartPlaceholder
v-if="myEnvironments.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
</HoppSmartPlaceholder>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
>
<div
v-if="teamListLoading"
class="flex flex-col items-center justify-center p-4"
>
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-else-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem
v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name"
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
:active-info-icon="isEnvActive(gen.id)"
@click="
() => {
handleEnvironmentChange(index, {
type: 'team-environment',
environment: gen,
})
hide()
}
"
/>
<HoppSmartPlaceholder
v-if="teamEnvironmentList.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
</HoppSmartPlaceholder>
</div>
<div
v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div>
</template>
</tippy>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue"
import { computed, onMounted, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
import IconEye from "~icons/lucide/eye"
import IconEdit from "~icons/lucide/edit"
import IconGlobe from "~icons/lucide/globe"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient"
import { useReadonlyStream, useStream } from "~/composables/stream"
import {
environments$,
globalEnv$,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import { workspaceStatus$ } from "~/newstore/workspace"
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { invokeAction } from "~/helpers/actions"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { Environment } from "@hoppscotch/data"
type Scope =
| {
type: "global"
}
| {
type: "my-environment"
environment: Environment
index: number
}
| {
type: "team-environment"
environment: TeamEnvironment
}
const props = defineProps<{
isScopeSelector?: boolean
modelValue?: Scope
}>()
const emit = defineEmits<{
(e: "update:modelValue", data: Scope): void
}>()
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
@@ -314,6 +212,39 @@ const myEnvironments = useReadonlyStream(environments$, [])
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// TeamList-Adapter
const teamListAdapter = new TeamListAdapter(true)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
REMEMBERED_TEAM_ID.value = team.id
changeWorkspace({
teamID: team.id,
teamName: team.name,
type: "team",
})
}
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) switchToTeamWorkspace(team)
}
}
}
)
// TeamEnv List Adapter
const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
@@ -348,41 +279,157 @@ watch(
}
)
const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
myEnvironments.value[selectedEnvironmentIndex.value.index]
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: environment.name,
variables: environment.variables,
const handleEnvironmentChange = (
index: number,
env?:
| {
type: "my-environment"
environment: Environment
}
| {
type: "team-environment"
environment: TeamEnvironment
}
) => {
if (props.isScopeSelector && env) {
if (env.type === "my-environment") {
emit("update:modelValue", {
type: "my-environment",
environment: env.environment,
index,
})
} else if (env.type === "team-environment") {
emit("update:modelValue", {
type: "team-environment",
environment: env.environment,
})
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
} else {
if (env && env.type === "my-environment") {
selectedEnvironmentIndex.value = {
type: "MY_ENV",
index,
}
} else if (env && env.type === "team-environment") {
selectedEnvironmentIndex.value = {
type: "TEAM_ENV",
teamEnvID: env.environment.id,
teamID: env.environment.teamID,
environment: env.environment.environment,
}
}
}
}
const isEnvActive = (id: string | number) => {
if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") {
return props.modelValue.index === id
} else if (props.modelValue?.type === "team-environment") {
return (
props.modelValue?.type === "team-environment" &&
props.modelValue.environment &&
props.modelValue.environment.id === id
)
}
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return selectedEnv.value.index === id
} else {
return (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnv.value.teamEnvID === id
)
}
}
}
const selectedEnv = computed(() => {
if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") {
return {
type: "MY_ENV",
index: props.modelValue.index,
name: props.modelValue.environment?.name,
}
} else if (props.modelValue?.type === "team-environment") {
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
name: props.modelValue.environment.environment.name,
teamEnvID: props.modelValue.environment.id,
}
} else {
return { type: "global", name: "Global" }
}
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
return { type: "NO_ENV_SELECTED" }
}
})
// Set the selected environment as initial scope value
onMounted(() => {
if (props.isScopeSelector) {
if (
selectedEnvironmentIndex.value.type === "MY_ENV" &&
selectedEnvironmentIndex.value.index !== undefined
) {
emit("update:modelValue", {
type: "my-environment",
environment: myEnvironments.value[selectedEnvironmentIndex.value.index],
index: selectedEnvironmentIndex.value.index,
})
} else if (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID &&
teamEnvironmentList.value &&
teamEnvironmentList.value.length > 0
) {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
emit("update:modelValue", {
type: "team-environment",
environment: teamEnv,
})
}
} else {
emit("update:modelValue", {
type: "global",
})
}
}
})
// Template refs
const envSelectorActions = ref<TippyComponent | null>(null)
const envQuickPeekActions = ref<TippyComponent | null>(null)
const tippyActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
@@ -396,32 +443,4 @@ const getErrorMessage = (err: GQLError<string>) => {
}
}
}
const globalEnvs = useReadonlyStream(globalEnv$, [])
const environmentVariables = computed(() => {
if (selectedEnv.value.variables) {
return selectedEnv.value.variables
} else {
return []
}
})
const editGlobalEnv = () => {
invokeAction("modals.my.environment.edit", {
envName: "Global",
})
}
const editEnv = () => {
if (selectedEnv.value.type === "MY_ENV" && selectedEnv.value.name) {
invokeAction("modals.my.environment.edit", {
envName: selectedEnv.value.name,
})
} else if (selectedEnv.value.type === "TEAM_ENV" && selectedEnv.value.name) {
invokeAction("modals.team.environment.edit", {
envName: selectedEnv.value.name,
})
}
}
</script>

View File

@@ -11,9 +11,9 @@
@edit-environment="editEnvironment('Global')"
/>
</div>
<EnvironmentsMy v-show="environmentType.type === 'my-environments'" />
<EnvironmentsMy v-if="environmentType.type === 'my-environments'" />
<EnvironmentsTeams
v-show="environmentType.type === 'team-environments'"
v-if="environmentType.type === 'team-environments'"
:team="environmentType.selectedTeam"
:team-environments="teamEnvironmentList"
:loading="loading"
@@ -198,15 +198,10 @@ const resetSelectedData = () => {
editingEnvironmentIndex.value = null
}
defineActionHandler("modals.environment.new", () => {
action.value = "new"
showModalDetails.value = true
})
defineActionHandler(
"modals.my.environment.edit",
({ envName, variableName }) => {
if (variableName) editingVariableName.value = variableName
editingVariableName.value = variableName
envName === "Global" && editEnvironment("Global")
}
)

View File

@@ -7,15 +7,22 @@
>
<template #body>
<div class="flex flex-col">
<HoppSmartInput
v-model="editingName"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
:disabled="editingEnvironmentIndex === 'Global'"
@submit="saveEnvironment"
/>
<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>
<div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}
@@ -171,7 +178,7 @@ const emit = defineEmits<{
const idTicker = ref(0)
const editingName = ref<string | null>(null)
const name = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } },
])
@@ -224,12 +231,10 @@ const liveEnvs = computed(() => {
}
if (props.editingEnvironmentIndex === "Global") {
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
]
return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))]
} else {
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
...vars.value.map((x) => ({ ...x.env, source: name.value! })),
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
]
}
@@ -239,7 +244,7 @@ watch(
() => props.show,
(show) => {
if (show) {
editingName.value = workingEnv.value?.name ?? null
name.value = workingEnv.value?.name ?? null
vars.value = pipe(
workingEnv.value?.variables ?? [],
A.map((e) => ({
@@ -272,7 +277,7 @@ const removeEnvironmentVariable = (index: number) => {
}
const saveEnvironment = () => {
if (!editingName.value) {
if (!name.value) {
toast.error(`${t("environment.invalid_name")}`)
return
}
@@ -288,13 +293,13 @@ const saveEnvironment = () => {
)
const environmentUpdated: Environment = {
name: editingName.value,
name: name.value,
variables: filterdVariables,
}
if (props.action === "new") {
// Creating a new environment
createEnvironment(editingName.value, environmentUpdated.variables)
createEnvironment(name.value, environmentUpdated.variables)
setSelectedEnvironmentIndex({
type: "MY_ENV",
index: envList.value.length - 1,
@@ -332,7 +337,7 @@ const saveEnvironment = () => {
}
const hideModal = () => {
editingName.value = null
name.value = null
emit("hide-modal")
}
</script>

View File

@@ -158,7 +158,5 @@ const duplicateEnvironments = () => {
cloneDeep(getGlobalVariables())
)
} else duplicateEnvironment(props.environmentIndex)
toast.success(`${t("environment.duplicated")}`)
}
</script>

View File

@@ -109,7 +109,7 @@ const resetSelectedData = () => {
defineActionHandler(
"modals.my.environment.edit",
({ envName, variableName }) => {
if (variableName) editingVariableName.value = variableName
editingVariableName.value = variableName
const envIndex: number = environments.value.findIndex(
(environment: Environment) => {
return environment.name === envName

View File

@@ -7,15 +7,23 @@
>
<template #body>
<div class="flex flex-col px-2">
<HoppSmartInput
v-model="editingName"
placeholder=" "
:input-styles="['floating-input', isViewer && 'opacity-25']"
:label="t('action.label')"
:disabled="isViewer"
@submit="saveEnvironment"
/>
<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>
<div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}
@@ -182,7 +190,7 @@ const emit = defineEmits<{
const idTicker = ref(0)
const editingName = ref<string | null>(null)
const name = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } },
])
@@ -208,9 +216,7 @@ const liveEnvs = computed(() => {
if (evnExpandError.value) {
return []
} else {
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
]
return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))]
}
})
@@ -219,7 +225,7 @@ watch(
(show) => {
if (show) {
if (props.action === "new") {
editingName.value = null
name.value = null
vars.value = pipe(
props.envVars() ?? [],
A.map((e: { key: string; value: string }) => ({
@@ -228,7 +234,7 @@ watch(
}))
)
} else if (props.editingEnvironment !== null) {
editingName.value = props.editingEnvironment.environment.name ?? null
name.value = props.editingEnvironment.environment.name ?? null
vars.value = pipe(
props.editingEnvironment.environment.variables ?? [],
A.map((e: { key: string; value: string }) => ({
@@ -266,7 +272,7 @@ const isLoading = ref(false)
const saveEnvironment = async () => {
isLoading.value = true
if (!editingName.value) {
if (!name.value) {
toast.error(`${t("environment.invalid_name")}`)
return
}
@@ -291,7 +297,7 @@ const saveEnvironment = async () => {
createTeamEnvironment(
JSON.stringify(filterdVariables),
props.editingTeamId,
editingName.value
name.value
),
TE.match(
(err: GQLError<string>) => {
@@ -314,7 +320,7 @@ const saveEnvironment = async () => {
updateTeamEnvironment(
JSON.stringify(filterdVariables),
props.editingEnvironment.id,
editingName.value
name.value
),
TE.match(
(err: GQLError<string>) => {
@@ -333,7 +339,7 @@ const saveEnvironment = async () => {
}
const hideModal = () => {
editingName.value = null
name.value = null
emit("hide-modal")
}

View File

@@ -154,7 +154,7 @@ const duplicateEnvironments = () => {
toast.error(`${getErrorMessage(err)}`)
},
() => {
toast.success(`${t("environment.duplicated")}`)
toast.success(`${t("team_environment.duplicate")}`)
}
)
)()

View File

@@ -178,7 +178,7 @@ const getErrorMessage = (err: GQLError<string>) => {
defineActionHandler(
"modals.team.environment.edit",
({ envName, variableName }) => {
if (variableName) editingVariableName.value = variableName
editingVariableName.value = variableName
const teamEnvToEdit = props.teamEnvironments.find(
(environment) => environment.environment.name === envName
)

View File

@@ -9,22 +9,27 @@
<template #body>
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
<HoppSmartItem
v-for="provider in allowedAuthProviders"
:key="provider.id"
:loading="provider.isLoading.value"
:icon="provider.icon"
:label="provider.label"
@click="provider.action"
:loading="signingInWithGitHub"
:icon="IconGithub"
:label="`${t('auth.continue_with_github')}`"
@click="signInWithGithub"
/>
<hr v-if="additonalLoginItems.length > 0" />
<HoppSmartItem
v-for="loginItem in additonalLoginItems"
:key="loginItem.id"
:icon="loginItem.icon"
:label="loginItem.text(t)"
@click="doAdditionalLoginItemClickAction(loginItem)"
:loading="signingInWithGoogle"
:icon="IconGoogle"
:label="`${t('auth.continue_with_google')}`"
@click="signInWithGoogle"
/>
<HoppSmartItem
:loading="signingInWithMicrosoft"
:icon="IconMicrosoft"
:label="`${t('auth.continue_with_microsoft')}`"
@click="signInWithMicrosoft"
/>
<HoppSmartItem
:icon="IconEmail"
:label="`${t('auth.continue_with_email')}`"
@click="mode = 'email'"
/>
</div>
<form
@@ -32,14 +37,24 @@
class="flex flex-col space-y-2"
@submit.prevent="signInWithEmail"
>
<HoppSmartInput
v-model="form.email"
type="email"
placeholder=" "
:label="t('auth.email')"
input-styles="floating-input"
/>
<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>
<HoppButtonPrimary
:loading="signingInWithEmail"
type="submit"
@@ -108,138 +123,124 @@
</HoppSmartModal>
</template>
<script setup lang="ts">
import { Ref, computed, onMounted, ref } from "vue"
import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
<script lang="ts">
import { defineComponent } from "vue"
import { platform } from "~/platform"
import { setLocalConfig } from "~/newstore/localpersistence"
import IconGithub from "~icons/auth/github"
import IconGoogle from "~icons/auth/google"
import IconEmail from "~icons/auth/email"
import IconMicrosoft from "~icons/auth/microsoft"
import IconArrowLeft from "~icons/lucide/arrow-left"
import { setLocalConfig } from "~/newstore/localpersistence"
import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { LoginItemDef } from "~/platform/auth"
export default defineComponent({
props: {
show: Boolean,
},
emits: ["hide-modal"],
setup() {
const { subscribeToStream } = useStreamSubscriber()
defineProps<{
show: boolean
}>()
const tosLink = import.meta.env.VITE_APP_TOS_LINK
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
return {
subscribeToStream,
t: useI18n(),
toast: useToast(),
IconGithub,
IconGoogle,
IconEmail,
IconMicrosoft,
IconArrowLeft,
tosLink,
privacyPolicyLink,
}
},
data() {
return {
form: {
email: "",
},
signingInWithGoogle: false,
signingInWithGitHub: false,
signingInWithMicrosoft: false,
signingInWithEmail: false,
mode: "sign-in",
}
},
mounted() {
const currentUser$ = platform.auth.getCurrentUserStream()
const { subscribeToStream } = useStreamSubscriber()
const t = useI18n()
const toast = useToast()
this.subscribeToStream(currentUser$, (user) => {
if (user) this.hideModal()
})
},
methods: {
showLoginSuccess() {
this.toast.success(`${this.t("auth.login_success")}`)
},
async signInWithGoogle() {
this.signingInWithGoogle = true
const form = {
email: "",
}
const signingInWithGoogle = ref(false)
const signingInWithGitHub = ref(false)
const signingInWithMicrosoft = ref(false)
const signingInWithEmail = ref(false)
const mode = ref("sign-in")
const tosLink = import.meta.env.VITE_APP_TOS_LINK
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
type AuthProviderItem = {
id: string
icon: typeof IconGithub
label: string
action: (...args: any[]) => any
isLoading: Ref<boolean>
}
const additonalLoginItems = computed(
() => platform.auth.additionalLoginItems ?? []
)
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
await item.onClick()
emit("hide-modal")
}
onMounted(() => {
const currentUser$ = platform.auth.getCurrentUserStream()
subscribeToStream(currentUser$, (user) => {
if (user) hideModal()
})
})
const showLoginSuccess = () => {
toast.success(`${t("auth.login_success")}`)
}
const signInWithGoogle = async () => {
signingInWithGoogle.value = true
try {
await platform.auth.signInUserWithGoogle()
} catch (e) {
console.error(e)
/*
try {
await platform.auth.signInUserWithGoogle()
} catch (e) {
console.error(e)
/*
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
*/
toast.error(`${t("error.something_went_wrong")}`)
}
this.toast.error(`${this.t("error.something_went_wrong")}`)
}
signingInWithGoogle.value = false
}
this.signingInWithGoogle = false
},
async signInWithGithub() {
this.signingInWithGitHub = true
const signInWithGithub = async () => {
signingInWithGitHub.value = true
const result = await platform.auth.signInUserWithGithub()
const result = await platform.auth.signInUserWithGithub()
if (!result) {
this.signingInWithGitHub = false
return
}
if (!result) {
signingInWithGitHub.value = false
return
}
if (result.type === "success") {
// this.showLoginSuccess()
} else if (result.type === "account-exists-with-different-cred") {
this.toast.info(`${this.t("auth.account_exists")}`, {
duration: 0,
closeOnSwipe: false,
action: {
text: `${this.t("action.yes")}`,
onClick: async (_, toastObject) => {
await result.link()
this.showLoginSuccess()
if (result.type === "success") {
// this.showLoginSuccess()
} else if (result.type === "account-exists-with-different-cred") {
toast.info(`${t("auth.account_exists")}`, {
duration: 0,
closeOnSwipe: false,
action: {
text: `${t("action.yes")}`,
onClick: async (_, toastObject) => {
await result.link()
showLoginSuccess()
toastObject.goAway(0)
},
},
})
} else {
console.log("error logging into github", result.err)
this.toast.error(`${this.t("error.something_went_wrong")}`)
}
toastObject.goAway(0)
},
},
})
} else {
console.log("error logging into github", result.err)
toast.error(`${t("error.something_went_wrong")}`)
}
this.signingInWithGitHub = false
},
async signInWithMicrosoft() {
this.signingInWithMicrosoft = true
signingInWithGitHub.value = false
}
const signInWithMicrosoft = async () => {
signingInWithMicrosoft.value = true
try {
await platform.auth.signInUserWithMicrosoft()
// this.showLoginSuccess()
} catch (e) {
console.error(e)
/*
try {
await platform.auth.signInUserWithMicrosoft()
// this.showLoginSuccess()
} catch (e) {
console.error(e)
/*
A auth/account-exists-with-different-credential Firebase error wont happen between MS with Google or Github
If a Github account exists and user then logs in with MS email we get a "Something went wrong toast" and console errors and MS replaces GH as only provider.
The error messages are as follows:
@@ -247,82 +248,34 @@ const signInWithMicrosoft = async () => {
@firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set
They may be related to https://github.com/firebase/firebaseui-web/issues/947
*/
toast.error(`${t("error.something_went_wrong")}`)
}
this.toast.error(`${this.t("error.something_went_wrong")}`)
}
signingInWithMicrosoft.value = false
}
const signInWithEmail = async () => {
signingInWithEmail.value = true
await platform.auth
.signInWithEmail(form.email)
.then(() => {
mode.value = "email-sent"
setLocalConfig("emailForSignIn", form.email)
})
.catch((e) => {
console.error(e)
toast.error(e.message)
signingInWithEmail.value = false
})
.finally(() => {
signingInWithEmail.value = false
})
}
const hideModal = () => {
mode.value = "sign-in"
toast.clear()
emit("hide-modal")
}
const authProviders: AuthProviderItem[] = [
{
id: "GITHUB",
icon: IconGithub,
label: t("auth.continue_with_github"),
action: signInWithGithub,
isLoading: signingInWithGitHub,
},
{
id: "GOOGLE",
icon: IconGoogle,
label: t("auth.continue_with_google"),
action: signInWithGoogle,
isLoading: signingInWithGoogle,
},
{
id: "MICROSOFT",
icon: IconMicrosoft,
label: t("auth.continue_with_microsoft"),
action: signInWithMicrosoft,
isLoading: signingInWithMicrosoft,
},
{
id: "EMAIL",
icon: IconEmail,
label: t("auth.continue_with_email"),
action: () => {
mode.value = "email"
this.signingInWithMicrosoft = false
},
async signInWithEmail() {
this.signingInWithEmail = true
await platform.auth
.signInWithEmail(this.form.email)
.then(() => {
this.mode = "email-sent"
setLocalConfig("emailForSignIn", this.form.email)
})
.catch((e) => {
console.error(e)
this.toast.error(e.message)
this.signingInWithEmail = false
})
.finally(() => {
this.signingInWithEmail = false
})
},
hideModal() {
this.mode = "sign-in"
this.toast.clear()
this.$emit("hide-modal")
},
isLoading: signingInWithEmail,
},
]
const allowedAuthProvidersIDsString: string | undefined = import.meta.env
.VITE_ALLOWED_AUTH_PROVIDERS
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
? allowedAuthProvidersIDsString.split(",")
: []
const allowedAuthProviders =
allowedAuthProvidersIDs.length > 0
? authProviders.filter((provider) =>
allowedAuthProvidersIDs.includes(provider.id)
)
: authProviders
})
</script>

View File

@@ -29,6 +29,7 @@
<script setup lang="ts">
import { platform } from "~/platform"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import {
@@ -37,13 +38,9 @@ import {
gqlURL$,
setGQLURL,
} from "~/newstore/GQLSession"
import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service"
const t = useI18n()
const interceptorService = useService(InterceptorService)
const props = defineProps<{
conn: GQLConnection
}>()
@@ -65,7 +62,7 @@ const onConnectClick = () => {
platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
platform: "graphql-schema",
strategy: interceptorService.currentInterceptorID.value!,
strategy: getCurrentStrategyID(),
})
} else {
props.conn.disconnect()

View File

@@ -373,6 +373,7 @@ import { commonHeaders } from "~/helpers/headers"
import { GQLConnection } from "~/helpers/GQLConnection"
import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
import { platform } from "~/platform"
import { getCurrentStrategyID } from "~/helpers/network"
import { useCodemirror } from "@composables/codemirror"
import jsonLinter from "~/helpers/editor/linting/json"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
@@ -380,8 +381,6 @@ import queryCompleter from "~/helpers/editor/completion/gqlQuery"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { objRemoveKey } from "~/helpers/functional/object"
import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service"
type OptionTabs = "query" | "headers" | "variables" | "authorization"
@@ -391,8 +390,6 @@ const selectedOptionTab = ref<OptionTabs>("query")
const t = useI18n()
const interceptorService = useService(InterceptorService)
const props = defineProps<{
conn: GQLConnection
}>()
@@ -747,7 +744,7 @@ const runQuery = async () => {
platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
platform: "graphql-query",
strategy: interceptorService.currentInterceptorID.value!,
strategy: getCurrentStrategyID(),
})
}
@@ -757,11 +754,7 @@ const hideRequestModal = () => {
const prettifyQuery = () => {
try {
gqlQueryString.value = gql.print(
gql.parse(gqlQueryString.value, {
allowLegacyFragmentVariables: true,
})
)
gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
prettifyQueryIcon.value = IconCheck
} catch (e) {
toast.error(`${t("error.gql_prettify_invalid_query")}`)

View File

@@ -105,7 +105,6 @@
@toggle-star="toggleStar(entry.entry)"
@delete-entry="deleteHistory(entry.entry)"
@use-entry="useHistory(toRaw(entry.entry))"
@add-to-collection="addToCollection(entry.entry)"
/>
</details>
</div>
@@ -177,7 +176,7 @@ import {
import HistoryRestCard from "./rest/Card.vue"
import HistoryGraphqlCard from "./graphql/Card.vue"
import { createNewTab } from "~/helpers/rest/tab"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { defineActionHandler } from "~/helpers/actions"
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
@@ -325,14 +324,6 @@ 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,8 +1,5 @@
<template>
<div
class="flex items-stretch group"
@contextmenu.prevent="options!.tippy.show()"
>
<div class="flex items-stretch group">
<span
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
@@ -29,39 +26,6 @@
{{ 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"
@@ -84,16 +48,15 @@
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
import { computed } 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
@@ -104,13 +67,8 @@ 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

@@ -28,9 +28,7 @@
>
<HoppSmartItem
:label="t('state.none')"
:info-icon="
(body.contentType === null ? IconDone : null) as any
"
:info-icon="(body.contentType === null ? IconDone : null) as any"
:active-info-icon="body.contentType === null"
@click="
() => {

View File

@@ -20,7 +20,7 @@
<span class="select-wrapper">
<HoppButtonSecondary
:label="
CodegenDefinitions.find((x) => x.name === codegenType)!.caption
CodegenDefinitions.find((x) => x.name === codegenType).caption
"
outline
class="flex-1 pr-8"
@@ -57,7 +57,12 @@
"
/>
<HoppSmartPlaceholder
v-if="filteredCodegenDefinitions.length === 0"
v-if="
!(
filteredCodegenDefinitions.length !== 0 ||
CodegenDefinitions.length === 0
)
"
:text="`${t('state.nothing_found')}${searchQuery}`"
>
<template #icon>

View File

@@ -79,13 +79,16 @@
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.key"
<HoppSmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
:auto-complete-source="commonHeaders"
:env-index="index"
:inspection-results="getInspectorResult(headerKeyResults, index)"
@change="
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles=" bg-transparent flex flex-1
py-1 px-4 truncate "
class="flex-1 !flex"
@input="
updateHeader(index, {
id: header.id,
key: $event,
@@ -97,10 +100,6 @@
<SmartEnvInput
v-model="header.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:inspection-results="
getInspectorResult(headerValueResults, index)
"
:env-index="index"
@change="
updateHeader(index, {
id: header.id,
@@ -266,9 +265,6 @@ import {
} from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab"
const t = useI18n()
const toast = useToast()
@@ -506,39 +502,4 @@ const changeTab = (tab: ComputedHeader["source"]) => {
if (tab === "auth") emit("change-tab", "authorization")
else emit("change-tab", "bodyParams")
}
const inspectionService = useService(InspectionService)
const allTabResults = inspectionService.tabs
const headerKeyResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
.filter(
(result) =>
result.locations.type === "header" &&
result.locations.position === "key"
) ?? []
)
})
const headerValueResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
.filter(
(result) =>
result.locations.type === "header" &&
result.locations.position === "value"
) ?? []
)
})
const getInspectorResult = (results: InspectorResult[], index: number) => {
return results.filter((result) => {
if (result.locations.type === "url" || result.locations.type === "response")
return
return result.locations.index === index
})
}
</script>

View File

@@ -82,9 +82,6 @@
<SmartEnvInput
v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
:inspection-results="
getInspectorResult(parameterKeyResults, index)
"
@change="
updateParam(index, {
id: param.id,
@@ -97,9 +94,6 @@
<SmartEnvInput
v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:inspection-results="
getInspectorResult(parameterValueResults, index)
"
@change="
updateParam(index, {
id: param.id,
@@ -179,7 +173,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
import IconCircle from "~icons/lucide/circle"
import IconTrash from "~icons/lucide/trash"
import IconWrapText from "~icons/lucide/wrap-text"
import { computed, reactive, ref, watch } from "vue"
import { reactive, ref, watch } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -201,9 +195,6 @@ import { useToast } from "@composables/toast"
import { throwError } from "@functional/error"
import { objRemoveKey } from "@functional/object"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab"
const colorMode = useColorMode()
@@ -407,39 +398,4 @@ const clearContent = () => {
bulkParams.value = ""
}
const inspectionService = useService(InspectionService)
const allTabResults = inspectionService.tabs
const parameterKeyResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
.filter(
(result) =>
result.locations.type === "parameter" &&
result.locations.position === "key"
) ?? []
)
})
const parameterValueResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
.filter(
(result) =>
result.locations.type === "parameter" &&
result.locations.position === "value"
) ?? []
)
})
const getInspectorResult = (results: InspectorResult[], index: number) => {
return results.filter((result) => {
if (result.locations.type === "url" || result.locations.type === "response")
return
return result.locations.index === index
})
}
</script>

View File

@@ -53,25 +53,16 @@
v-model="tab.document.request.endpoint"
:placeholder="`${t('request.url')}`"
:auto-complete-source="userHistories"
:inspection-results="tabResults"
@paste="onPasteUrl($event)"
@enter="newSendRequest"
>
<template #empty>
<span>
{{ t("empty.history_suggestions") }}
</span>
</template>
</SmartEnvInput>
/>
</div>
</div>
<div class="flex mt-2 sm:mt-0">
<HoppButtonPrimary
id="send"
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'action.send'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:title="`${t('action.send')} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
class="flex-1 rounded-r-none min-w-20"
@click="!loading ? newSendRequest() : cancelRequest()"
@@ -230,7 +221,6 @@
v-if="showSaveRequestModal"
mode="rest"
:show="showSaveRequestModal"
:request="request"
@hide-modal="showSaveRequestModal = false"
/>
</div>
@@ -243,12 +233,17 @@ import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { refAutoReset, useVModel } from "@vueuse/core"
import * as E from "fp-ts/Either"
import { Ref, computed, onBeforeUnmount, ref } from "vue"
import { isLeft, isRight } from "fp-ts/lib/Either"
import { computed, onBeforeUnmount, ref } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import {
cancelRunningExtensionRequest,
hasExtensionInstalled,
} from "~/helpers/strategies/ExtensionStrategy"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { copyToClipboard } from "~/helpers/utils/clipboard"
@@ -263,17 +258,13 @@ import IconLink2 from "~icons/lucide/link-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save"
import IconShare2 from "~icons/lucide/share-2"
import { HoppRESTTab, currentTabID } from "~/helpers/rest/tab"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
import { platform } from "~/platform"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service"
import { getCurrentStrategyID } from "~/helpers/network"
const t = useI18n()
const interceptorService = useService(InterceptorService)
const methods = [
"GET",
@@ -326,8 +317,6 @@ const saveRequestAction = ref<any | null>(null)
const history = useReadonlyStream<RESTHistoryEntry[]>(restHistory$, [])
const requestCancelFunc: Ref<(() => void) | null> = ref(null)
const userHistories = computed(() => {
return history.value.map((history) => history.request.endpoint).slice(0, 10)
})
@@ -346,15 +335,13 @@ const newSendRequest = async () => {
platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
platform: "rest",
strategy: interceptorService.currentInterceptorID.value!,
strategy: getCurrentStrategyID(),
})
const [cancel, streamPromise] = runRESTRequest$(tab)
const streamResult = await streamPromise
// Double calling is because the function returns a TaskEither than should be executed
const streamResult = await runRESTRequest$(tab)()
requestCancelFunc.value = cancel
if (E.isRight(streamResult)) {
if (isRight(streamResult)) {
subscribeToStream(
streamResult.right,
(responseState) => {
@@ -371,7 +358,7 @@ const newSendRequest = async () => {
loading.value = false
}
)
} else {
} else if (isLeft(streamResult)) {
loading.value = false
toast.error(`${t("error.script_fail")}`)
let error: Error
@@ -421,8 +408,9 @@ function isCURL(curl: string) {
const cancelRequest = () => {
loading.value = false
requestCancelFunc.value?.()
if (hasExtensionInstalled()) {
cancelRunningExtensionRequest()
}
updateRESTResponse(null)
}
@@ -590,8 +578,6 @@ const saveRequest = () => {
}
}
const request = ref<HoppRESTRequest | null>(null)
onBeforeUnmount(() => {
if (loading.value) cancelRequest()
})
@@ -607,22 +593,7 @@ defineActionHandler("request.method.prev", cycleUpMethod)
defineActionHandler("request.save", saveRequest)
defineActionHandler(
"request.save-as",
(
req:
| {
requestType: "rest"
request: HoppRESTRequest
}
| {
requestType: "gql"
request: HoppGQLRequest
}
) => {
showSaveRequestModal.value = true
if (req && req.requestType === "rest") {
request.value = req.request
}
}
() => (showSaveRequestModal.value = true)
)
defineActionHandler("request.method.get", () => updateMethod("GET"))
defineActionHandler("request.method.post", () => updateMethod("POST"))
@@ -630,13 +601,6 @@ defineActionHandler("request.method.put", () => updateMethod("PUT"))
defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
defineActionHandler("request.method.head", () => updateMethod("HEAD"))
defineActionHandler("request.import-curl", () => {
showCurlImportModal.value = true
})
defineActionHandler("request.show-code", () => {
showCodegenModal.value = true
})
const isCustomMethod = computed(() => {
return (
tab.value.document.request.method === "CUSTOM" ||
@@ -645,12 +609,4 @@ const isCustomMethod = computed(() => {
})
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService)
const allTabResults = inspectionService.tabs
const tabResults = computed(() => {
return allTabResults.value.get(currentTabID.value) ?? []
})
</script>

View File

@@ -1,6 +1,6 @@
<template>
<HoppSmartTabs
v-model="selectedOptionsTab"
v-model="selectedRealtimeTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
render-inactive-tabs
>
@@ -56,15 +56,12 @@ import { useI18n } from "@composables/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { computed, ref } from "vue"
import { defineActionHandler } from "~/helpers/actions"
export type RequestOptionTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "preRequestScript"
| "tests"
const t = useI18n()
@@ -76,10 +73,10 @@ const emit = defineEmits<{
const request = useVModel(props, "modelValue", emit)
const selectedOptionsTab = ref<RequestOptionTabs>("params")
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
const changeTab = (e: RequestOptionTabs) => {
selectedOptionsTab.value = e
selectedRealtimeTab.value = e
}
const newActiveParamsCount$ = computed(() => {
@@ -99,8 +96,4 @@ const newActiveHeadersCount$ = computed(() => {
if (e === 0) return null
return `${e}`
})
defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionsTab.value = tab
})
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col flex-1 relative">
<div class="flex flex-col flex-1">
<HttpResponseMeta :response="tab.response" />
<LensesResponseBodyRenderer
v-if="!loading && hasResponse"

View File

@@ -1,6 +1,6 @@
<template>
<div
class="sticky top-0 z-10 flex items-center justify-center flex-shrink-0 p-4 overflow-auto overflow-x-auto bg-primary whitespace-nowrap"
class="sticky top-0 z-10 flex items-start justify-center flex-shrink-0 p-4 overflow-auto overflow-x-auto bg-primary whitespace-nowrap"
>
<AppShortcutsPrompt v-if="response == null" class="flex-1" />
<div v-else class="flex flex-col flex-1">
@@ -17,7 +17,6 @@
:alt="`${t('error.network_fail')}`"
:heading="t('error.network_fail')"
:text="t('helpers.network_fail')"
large
>
<AppInterceptor class="p-2 border rounded border-dividerLight" />
</HoppSmartPlaceholder>
@@ -27,7 +26,6 @@
:alt="`${t('error.script_fail')}`"
:label="t('error.script_fail')"
:text="t('helpers.script_fail')"
large
>
<div
class="mt-2 w-full px-4 py-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
@@ -72,15 +70,6 @@
</div>
</div>
</div>
<AppInspection
v-if="response?.type !== 'loading'"
:inspection-results="tabResults"
:class="[
response === null || response?.type === 'network_fail'
? 'absolute right-2 top-2'
: 'ml-2 -m-2',
]"
/>
</div>
</template>
@@ -91,9 +80,6 @@ import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab"
const t = useI18n()
const colorMode = useColorMode()
@@ -142,16 +128,4 @@ const statusCategory = computed(() => {
}
return findStatusGroup(props.response.statusCode)
})
const inspectionService = useService(InspectionService)
const allTabResults = inspectionService.tabs
const tabResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
?.filter((result) => result.locations.type === "response") ?? []
)
})
</script>

View File

@@ -1,126 +0,0 @@
<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 truncate">
{{ 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

@@ -1,39 +0,0 @@
<template>
<div
v-if="
interceptorSelection === extensionService.interceptorID &&
extensionService.extensionStatus.value !== 'available'
"
class="flex space-x-2"
>
<HoppButtonSecondary
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
:icon="IconChrome"
label="Chrome"
outline
class="!flex-1"
/>
<HoppButtonSecondary
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
:icon="IconFirefox"
label="Firefox"
outline
class="!flex-1"
/>
</div>
</template>
<script setup lang="ts">
import IconChrome from "~icons/brands/chrome"
import IconFirefox from "~icons/brands/firefox"
import { InterceptorService } from "~/services/interceptor.service"
import { useService } from "dioc/vue"
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
const interceptorService = useService(InterceptorService)
const extensionService = useService(ExtensionInterceptorService)
const interceptorSelection = interceptorService.currentInterceptorID
</script>

View File

@@ -131,7 +131,6 @@ const fetchMyTeams = async () => {
loading.value = true
const result = await runGQLQuery({
query: GetMyTeamsDocument,
variables: {},
})
loading.value = false

View File

@@ -35,7 +35,7 @@
<div class="relative flex flex-col">
<input
id="selectLabelAdd"
v-model="editingName"
v-model="name"
v-focus
class="input floating-input"
placeholder=" "
@@ -113,13 +113,13 @@ const emit = defineEmits<{
}>()
const QoS = ref<(typeof QOS_VALUES)[number]>(2)
const editingName = ref("")
const name = ref("")
const color = ref("#f58290")
watch(
() => props.show,
() => {
editingName.value = ""
name.value = ""
QoS.value = 2
const randomColor = Math.floor(Math.random() * 16777215).toString(16)
color.value = `#${randomColor}`
@@ -127,18 +127,18 @@ watch(
)
const addNewSubscription = () => {
if (!editingName.value) {
if (!name.value) {
toastr.error(t("mqtt.invalid_topic").toString())
return
}
emit("submit", {
name: editingName.value,
name: name.value,
qos: QoS.value,
color: color.value,
})
}
const hideModal = () => {
editingName.value = ""
name.value = ""
QoS.value = 2
emit("hide-modal")
}

View File

@@ -1,88 +0,0 @@
<template>
<div class="my-1 text-secondaryLight">
<span v-if="extensionVersion != null">
{{
`${t("settings.extension_version")}: v${extensionVersion.major}.${
extensionVersion.minor
}`
}}
</span>
<span v-else>
{{ t("settings.extension_version") }}:
{{ t("settings.extension_ver_not_reported") }}
</span>
</div>
<div class="flex flex-col py-4 space-y-2">
<span>
<HoppSmartItem
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
:icon="IconChrome"
label="Chrome"
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasChromeExtInstalled"
outline
/>
</span>
<span>
<HoppSmartItem
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
:icon="IconFirefox"
label="Firefox"
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasFirefoxExtInstalled"
outline
/>
</span>
</div>
<div class="py-4 space-y-4">
<div class="flex items-center">
<HoppSmartToggle
:on="extensionEnabled"
@change="extensionEnabled = !extensionEnabled"
>
{{ t("settings.extensions_use_toggle") }}
</HoppSmartToggle>
</div>
</div>
</template>
<script setup lang="ts">
import IconChrome from "~icons/brands/chrome"
import IconFirefox from "~icons/brands/firefox"
import IconCheckCircle from "~icons/lucide/check-circle"
import { useI18n } from "@composables/i18n"
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
import { useService } from "dioc/vue"
import { computed } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
import { platform } from "~/platform"
const t = useI18n()
const interceptorService = useService(InterceptorService)
const extensionService = useService(ExtensionInterceptorService)
const extensionVersion = extensionService.extensionVersion
const hasChromeExtInstalled = extensionService.chromeExtensionInstalled
const hasFirefoxExtInstalled = extensionService.firefoxExtensionInstalled
const extensionEnabled = computed({
get() {
return (
interceptorService.currentInterceptorID.value ===
extensionService.interceptorID
)
},
set(active) {
if (active) {
interceptorService.currentInterceptorID.value =
extensionService.interceptorID
} else {
interceptorService.currentInterceptorID.value =
platform.interceptors.default
}
},
})
</script>

View File

@@ -1,94 +0,0 @@
<template>
<div class="my-1 text-secondaryLight">
{{ `${t("settings.official_proxy_hosting")} ${t("settings.read_the")}` }}
<HoppSmartAnchor
class="link"
to="https://docs.hoppscotch.io/support/privacy"
blank
:label="t('app.proxy_privacy_policy')"
/>.
</div>
<div class="py-4 space-y-4">
<div class="flex items-center">
<HoppSmartToggle
:on="proxyEnabled"
@change="proxyEnabled = !proxyEnabled"
>
{{ t("settings.proxy_use_toggle") }}
</HoppSmartToggle>
</div>
</div>
<div class="flex items-center py-4 space-x-2">
<HoppSmartInput
v-model="PROXY_URL"
styles="flex-1"
placeholder=" "
input-styles="input floating-input"
:disabled="!proxyEnabled"
>
<template #label>
<label for="url">
{{ t("settings.proxy_url") }}
</label>
</template>
</HoppSmartInput>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('settings.reset_default')"
:icon="clearIcon"
outline
class="rounded"
@click="resetProxy"
/>
</div>
</template>
<script setup lang="ts">
import { refAutoReset } from "@vueuse/core"
import { useI18n } from "~/composables/i18n"
import { useSetting } from "~/composables/settings"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconCheck from "~icons/lucide/check"
import { useToast } from "~/composables/toast"
import { computed } from "vue"
import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service"
import { proxyInterceptor } from "~/platform/std/interceptors/proxy"
import { platform } from "~/platform"
const t = useI18n()
const toast = useToast()
const interceptorService = useService(InterceptorService)
const PROXY_URL = useSetting("PROXY_URL")
const proxyEnabled = computed({
get() {
return (
interceptorService.currentInterceptorID.value ===
proxyInterceptor.interceptorID
)
},
set(active) {
if (active) {
interceptorService.currentInterceptorID.value =
proxyInterceptor.interceptorID
} else {
interceptorService.currentInterceptorID.value =
platform.interceptors.default
}
},
})
const clearIcon = refAutoReset<typeof IconRotateCCW | typeof IconCheck>(
IconRotateCCW,
1000
)
const resetProxy = () => {
PROXY_URL.value = "https://proxy.hoppscotch.io/"
clearIcon.value = IconCheck
toast.success(`${t("state.cleared")}`)
}
</script>

View File

@@ -18,13 +18,15 @@
</span>
<template #content="{ hide }">
<div class="flex flex-col space-y-2">
<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 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>
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"

View File

@@ -1,8 +1,6 @@
<template>
<div class="autocomplete-wrapper">
<div
class="absolute inset-0 flex flex-1 divide-x divide-dividerLight overflow-x-auto"
>
<div class="absolute inset-0 flex flex-1 overflow-x-auto">
<div
ref="editor"
:placeholder="placeholder"
@@ -12,10 +10,6 @@
@keydown="handleKeystroke"
@focusin="showSuggestionPopover = true"
></div>
<AppInspection
:inspection-results="inspectionResults"
class="sticky inset-y-0 right-0 bg-primary rounded-r"
/>
</div>
<ul
v-if="showSuggestionPopover && autoCompleteSource"
@@ -40,11 +34,8 @@
</div>
</li>
<li v-if="suggestions.length === 0" class="pointer-events-none">
<div v-if="slots.empty" class="truncate py-0.5">
<slot name="empty"></slot>
</div>
<span v-else class="truncate py-0.5">
{{ t("empty.suggestions") }}
<span class="truncate py-0.5">
{{ t("empty.history_suggestions") }}
</span>
</li>
</ul>
@@ -52,7 +43,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick, computed, Ref, useSlots } from "vue"
import { ref, onMounted, watch, nextTick, computed, Ref } from "vue"
import {
EditorView,
placeholder as placeholderExt,
@@ -71,7 +62,6 @@ import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { platform } from "~/platform"
import { useI18n } from "~/composables/i18n"
import { onClickOutside, useDebounceFn } from "@vueuse/core"
import { InspectorResult } from "~/services/inspection"
import { invokeAction } from "~/helpers/actions"
const props = withDefaults(
@@ -85,7 +75,6 @@ const props = withDefaults(
environmentHighlights?: boolean
readonly?: boolean
autoCompleteSource?: string[]
inspectionResults?: InspectorResult[] | undefined
}>(),
{
modelValue: "",
@@ -96,8 +85,6 @@ const props = withDefaults(
readonly: false,
environmentHighlights: true,
autoCompleteSource: undefined,
inspectionResult: undefined,
inspectionResults: undefined,
}
)
@@ -111,8 +98,6 @@ const emit = defineEmits<{
(e: "click", ev: any): void
}>()
const slots = useSlots()
const t = useI18n()
const cachedValue = ref(props.modelValue)
@@ -157,9 +142,7 @@ const suggestions = computed(() => {
const updateModelValue = (value: string) => {
emit("update:modelValue", value)
emit("change", value)
nextTick(() => {
showSuggestionPopover.value = false
})
showSuggestionPopover.value = false
}
const handleKeystroke = (ev: KeyboardEvent) => {
@@ -359,6 +342,7 @@ 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

@@ -22,10 +22,9 @@
:node="node as TreeNode<T>"
:toggle-children="toggleChildren as () => void"
:is-open="isOpen as boolean"
:highlight-children="(id: string | null) => highlightChildren(id)"
:highlight-children="(id:string|null) => highlightChildren(id)"
></slot>
</template>
<template #emptyNode="{ node }">
<slot name="emptyNode" :node="node"></slot>
</template>
@@ -36,26 +35,22 @@
v-else-if="rootNodes.status === 'loading'"
class="flex flex-col items-center justify-center flex-1 p-4"
>
<SmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t?.("state.loading") }}</span>
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div
v-if="rootNodes.status === 'loaded' && rootNodes.data.length === 0"
class="flex flex-col flex-1"
>
<!-- eslint-disable-next-line vue/no-deprecated-filter -->
<slot name="emptyNode" :node="null as TreeNode<T> | null"></slot>
<slot name="emptyNode" :node="(null as TreeNode<T> | null)"></slot>
</div>
</div>
</template>
<script setup lang="ts" generic="T extends any">
import { computed, inject } from "vue"
import SmartTreeBranch from "./TreeBranch.vue"
import SmartSpinner from "./Spinner.vue"
import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
import { HOPP_UI_OPTIONS, HoppUIPluginOptions } from "./../../plugin"
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
const props = defineProps<{
/**
@@ -65,6 +60,8 @@ const props = defineProps<{
adapter: SmartTreeAdapter<T>
}>()
const t = useI18n()
/**
* Fetch the root nodes from the adapter by passing the node id as null
*/

View File

@@ -3,7 +3,7 @@
:node="nodeItem"
:toggle-children="toggleNodeChildren"
:is-open="isNodeOpen"
:highlight-children="(id: string | null) => highlightNodeChildren(id)"
:highlight-children="(id:string|null) => highlightNodeChildren(id)"
></slot>
<!-- This is a performance optimization trick -->
@@ -25,7 +25,7 @@
'bg-divider': highlightNode,
}"
>
<SmartTreeBranch
<TreeBranch
v-for="childNode in childNodes.data"
:key="childNode.id"
:node-item="childNode"
@@ -45,23 +45,21 @@
:node="node as TreeNode<T>"
:toggle-children="toggleChildren as () => void"
:is-open="isOpen as boolean"
:highlight-children="
(id: string | null) => highlightChildren(id) as void
"
:highlight-children="(id:string|null) => highlightChildren(id) as void"
></slot>
</template>
<template #emptyNode="{ node }">
<slot name="emptyNode" :node="node"></slot>
</template>
</SmartTreeBranch>
</TreeBranch>
</div>
<div
v-if="childNodes.status === 'loading'"
class="flex flex-col items-center justify-center flex-1 p-4"
>
<SmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t?.("state.loading") }}</span>
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div
v-if="childNodes.status === 'loaded' && childNodes.data.length === 0"
@@ -73,12 +71,9 @@
</template>
<script setup lang="ts" generic="T extends any">
import { computed, inject, ref } from "vue"
import SmartTreeBranch from "./TreeBranch.vue"
import SmartSpinner from "./Spinner.vue"
import { computed, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
import { HOPP_UI_OPTIONS, HoppUIPluginOptions } from "./../../plugin"
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
const props = defineProps<{
/**
@@ -97,6 +92,7 @@ const props = defineProps<{
}>()
const CHILD_SLOT_NAME = "default"
const t = useI18n()
const isOnlyRootChild = computed(() => props.rootNodesLength === 1)

View File

@@ -1,13 +1,21 @@
<template>
<HoppSmartModal v-if="show" dialog :title="t('team.new')" @close="hideModal">
<template #body>
<HoppSmartInput
v-model="editingName"
:label="t('action.label')"
placeholder=" "
input-styles="floating-input"
@submit="addNewTeam"
/>
<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>
</template>
<template #footer>
<span class="flex space-x-2">
@@ -50,14 +58,14 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const editingName = ref<string | null>(null)
const name = ref<string | null>(null)
const isLoading = ref(false)
const addNewTeam = async () => {
isLoading.value = true
await pipe(
TeamNameCodec.decode(editingName.value),
TeamNameCodec.decode(name.value),
TE.fromEither,
TE.mapLeft(() => "invalid_name" as const),
TE.chainW(createTeam),
@@ -86,7 +94,7 @@ const addNewTeam = async () => {
}
const hideModal = () => {
editingName.value = null
name.value = null
emit("hide-modal")
}
</script>

View File

@@ -2,13 +2,21 @@
<HoppSmartModal v-if="show" dialog :title="t('team.edit')" @close="hideModal">
<template #body>
<div class="flex flex-col">
<HoppSmartInput
v-model="editingName"
placeholder=" "
:label="t('action.label')"
input-styles="floating-input"
@submit="saveTeam"
/>
<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>
<div class="flex items-center justify-between flex-1 pt-4">
<label for="memberList" class="p-4">
{{ t("team.members") }}
@@ -177,7 +185,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue"
import { computed, ref, toRef, watch } from "vue"
import * as E from "fp-ts/Either"
import {
GetTeamDocument,
@@ -228,12 +236,12 @@ const props = defineProps<{
const toast = useToast()
const editingName = ref(props.editingTeam.name)
const name = toRef(props.editingTeam, "name")
watch(
() => props.editingTeam.name,
(newName: string) => {
editingName.value = newName
name.value = newName
}
)
@@ -381,11 +389,11 @@ const isLoading = ref(false)
const saveTeam = async () => {
isLoading.value = true
if (editingName.value !== "") {
if (TeamNameCodec.is(editingName.value)) {
if (name.value !== "") {
if (TeamNameCodec.is(name.value)) {
const updateTeamNameResult = await renameTeam(
props.editingTeamID,
editingName.value
name.value
)()
if (E.isLeft(updateTeamNameResult)) {
toast.error(`${t("error.something_went_wrong")}`)

View File

@@ -82,7 +82,6 @@ import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconDone from "~icons/lucide/check"
import { useLocalState } from "~/newstore/localstate"
import { defineActionHandler } from "~/helpers/actions"
const t = useI18n()
const colorMode = useColorMode()
@@ -155,14 +154,4 @@ const displayModalAdd = (shouldDisplay: boolean) => {
showModalAdd.value = shouldDisplay
teamListadapter.fetchList()
}
defineActionHandler("modals.team.new", () => {
displayModalAdd(true)
})
defineActionHandler("workspace.switch.personal", switchToPersonalWorkspace)
defineActionHandler("workspace.switch", ({ teamId }) => {
const team = myTeams.value.find((t) => t.id === teamId)
if (team) switchToTeamWorkspace(team)
})
</script>

View File

@@ -17,7 +17,6 @@ import {
parseGQLErrorString,
} from "@helpers/backend/GQLClient"
import {
AnyVariables,
createRequest,
GraphQLRequest,
OperationResult,
@@ -36,11 +35,7 @@ type UseQueryOptions<T = any, V = object> = {
pollDuration?: number | undefined
}
export const useGQLQuery = <
DocType,
DocVarType extends AnyVariables,
DocErrorType extends string,
>(
export const useGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
_args: UseQueryOptions<DocType, DocVarType>
) => {
const stops: WatchStopHandle[] = []

View File

@@ -1,4 +1,3 @@
import * as E from "fp-ts/Either"
import { BehaviorSubject } from "rxjs"
import {
getIntrospectionQuery,
@@ -12,8 +11,7 @@ import {
} from "graphql"
import { distinctUntilChanged, map } from "rxjs/operators"
import { GQLHeader, HoppGQLAuth } from "@hoppscotch/data"
import { getService } from "~/modules/dioc"
import { InterceptorService } from "~/services/interceptor.service"
import { sendNetworkRequest } from "./network"
const GQL_SCHEMA_POLL_INTERVAL = 7000
@@ -31,7 +29,9 @@ export class GQLConnection {
map((schema) => {
if (!schema) return null
return printSchema(schema)
return printSchema(schema, {
commentDescriptions: true,
})
})
)
@@ -181,7 +181,7 @@ export class GQLConnection {
headers.forEach((x) => (finalHeaders[x.key] = x.value))
const reqOptions = {
method: "POST" as const,
method: "POST",
url,
headers: {
...finalHeaders,
@@ -190,20 +190,11 @@ export class GQLConnection {
data: introspectionQuery,
}
const interceptorService = getService(InterceptorService)
const res = await interceptorService.runRequest(reqOptions).response
if (E.isLeft(res)) {
console.error(res.left)
throw new Error(res.left.toString())
}
const data = res.right
const data = await sendNetworkRequest(reqOptions)
// HACK : Temporary trailing null character issue from the extension fix
const response = new TextDecoder("utf-8")
.decode(data.data as any)
.decode(data.data)
.replace(/\0+$/, "")
const introspectResponse = JSON.parse(response)
@@ -254,7 +245,7 @@ export class GQLConnection {
.forEach(({ key, value }) => (finalHeaders[key] = value))
const reqOptions = {
method: "POST" as const,
method: "POST",
url,
headers: {
...finalHeaders,
@@ -269,19 +260,11 @@ export class GQLConnection {
},
}
const interceptorService = getService(InterceptorService)
const result = await interceptorService.runRequest(reqOptions).response
if (E.isLeft(result)) {
console.error(result.left)
throw new Error(result.left.toString())
}
const res = result.right
const res = await sendNetworkRequest(reqOptions)
// HACK: Temporary trailing null character issue from the extension fix
const responseText = new TextDecoder("utf-8")
.decode(res.data as any)
.decode(res.data)
.replace(/\0+$/, "")
return responseText

View File

@@ -1,5 +1,6 @@
import { Observable, Subject } from "rxjs"
import { filter } from "rxjs/operators"
import * as TE from "fp-ts/lib/TaskEither"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -9,7 +10,7 @@ import {
runTestScript,
TestDescriptor,
} from "@hoppscotch/js-sandbox"
import * as E from "fp-ts/Either"
import { isRight } from "fp-ts/Either"
import { cloneDeep } from "lodash-es"
import {
getCombinedEnvVariables,
@@ -68,130 +69,106 @@ export const executedResponses$ = new Subject<
HoppRESTResponse & { type: "success" | "fail " }
>()
export function runRESTRequest$(
export const runRESTRequest$ = (
tab: Ref<HoppRESTTab>
): [
() => void,
Promise<
| E.Left<"script_fail" | "cancellation">
| E.Right<Observable<HoppRESTResponse>>
>,
] {
let cancelCalled = false
let cancelFunc: (() => void) | null = null
): TE.TaskEither<string | Error, Observable<HoppRESTResponse>> =>
pipe(
getFinalEnvsFromPreRequest(
tab.value.document.request.preRequestScript,
getCombinedEnvVariables()
),
TE.chain((envs) => {
const effectiveRequest = getEffectiveRESTRequest(
tab.value.document.request,
{
name: "Env",
variables: combineEnvVariables(envs),
}
)
const cancel = () => {
cancelCalled = true
cancelFunc?.()
}
const stream = createRESTNetworkRequestStream(effectiveRequest)
const res = getFinalEnvsFromPreRequest(
tab.value.document.request.preRequestScript,
getCombinedEnvVariables()
)().then((envs) => {
if (cancelCalled) return E.left("cancellation" as const)
// Run Test Script when request ran successfully
const subscription = stream
.pipe(filter((res) => res.type === "success" || res.type === "fail"))
.subscribe(async (res) => {
if (res.type === "success" || res.type === "fail") {
executedResponses$.next(
// @ts-expect-error Typescript can't figure out this inference for some reason
res
)
if (E.isLeft(envs)) {
console.error(envs.left)
return E.left("script_fail" as const)
}
const effectiveRequest = getEffectiveRESTRequest(
tab.value.document.request,
{
name: "Env",
variables: combineEnvVariables(envs.right),
}
)
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
cancelFunc = cancelRun
const subscription = stream
.pipe(filter((res) => res.type === "success" || res.type === "fail"))
.subscribe(async (res) => {
if (res.type === "success" || res.type === "fail") {
executedResponses$.next(
// @ts-expect-error Typescript can't figure out this inference for some reason
res
)
const runResult = await runTestScript(
res.req.testScript,
envs.right,
{
const runResult = await runTestScript(res.req.testScript, envs, {
status: res.statusCode,
body: getTestableBody(res),
headers: res.headers,
}
)()
})()
if (E.isRight(runResult)) {
tab.value.testResults = translateToSandboxTestResults(
runResult.right
)
setGlobalEnvVariables(runResult.right.envs.global)
if (
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
) {
const env = getEnvironment({
type: "MY_ENV",
index: environmentsStore.value.selectedEnvironmentIndex.index,
})
updateEnvironment(
environmentsStore.value.selectedEnvironmentIndex.index,
{
...env,
variables: runResult.right.envs.selected,
}
if (isRight(runResult)) {
tab.value.testResults = translateToSandboxTestResults(
runResult.right
)
} else if (
environmentsStore.value.selectedEnvironmentIndex.type ===
"TEAM_ENV"
) {
const env = getEnvironment({
type: "TEAM_ENV",
})
pipe(
updateTeamEnvironment(
JSON.stringify(runResult.right.envs.selected),
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
env.name
setGlobalEnvVariables(runResult.right.envs.global)
if (
environmentsStore.value.selectedEnvironmentIndex.type ===
"MY_ENV"
) {
const env = getEnvironment({
type: "MY_ENV",
index: environmentsStore.value.selectedEnvironmentIndex.index,
})
updateEnvironment(
environmentsStore.value.selectedEnvironmentIndex.index,
{
...env,
variables: runResult.right.envs.selected,
}
)
)()
}
} else {
tab.value.testResults = {
description: "",
expectResults: [],
tests: [],
envDiff: {
global: {
additions: [],
deletions: [],
updations: [],
} else if (
environmentsStore.value.selectedEnvironmentIndex.type ===
"TEAM_ENV"
) {
const env = getEnvironment({
type: "TEAM_ENV",
})
pipe(
updateTeamEnvironment(
JSON.stringify(runResult.right.envs.selected),
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
env.name
)
)()
}
} else {
tab.value.testResults = {
description: "",
expectResults: [],
tests: [],
envDiff: {
global: {
additions: [],
deletions: [],
updations: [],
},
selected: {
additions: [],
deletions: [],
updations: [],
},
},
selected: {
additions: [],
deletions: [],
updations: [],
},
},
scriptError: true,
scriptError: true,
}
}
subscription.unsubscribe()
}
})
subscription.unsubscribe()
}
})
return E.right(stream)
})
return [cancel, res]
}
return TE.right(stream)
})
)
const getAddedEnvVariables = (
current: Environment["variables"],

View File

@@ -5,8 +5,7 @@
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
import { BehaviorSubject } from "rxjs"
import { HoppRESTDocument } from "./rest/document"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
import { HoppGQLRequest } from "@hoppscotch/data"
export type HoppAction =
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
@@ -15,7 +14,6 @@ export type HoppAction =
| "request.copy-link" // Copy Request Link
| "request.save" // Save to Collections
| "request.save-as" // Save As
| "rest.request.rename" // Rename
| "request.method.next" // Select Next Method
| "request.method.prev" // Select Previous Method
| "request.method.get" // Select GET Method
@@ -23,22 +21,13 @@ export type HoppAction =
| "request.method.post" // Select POST Method
| "request.method.put" // Select PUT Method
| "request.method.delete" // Select DELETE Method
| "request.import-curl" // Import cURL
| "request.show-code" // Show generated code
| "flyouts.chat.open" // Shows the keybinds flyout
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
| "modals.search.toggle" // Shows the search modal
| "modals.support.toggle" // Shows the support modal
| "modals.share.toggle" // Shows the share modal
| "modals.social.toggle" // Shows the social links modal
| "modals.environment.add" // Show add environment modal via context menu
| "modals.environment.new" // Add new environment
| "modals.my.environment.edit" // Edit current personal environment
| "modals.team.environment.edit" // Edit current team environment
| "modals.team.new" // Add new team
| "modals.team.edit" // Edit selected team
| "modals.team.invite" // Invite selected team
| "workspace.switch.personal" // Switch to personal workspace
| "navigation.jump.rest" // Jump to REST page
| "navigation.jump.graphql" // Jump to GraphQL page
| "navigation.jump.realtime" // Jump to realtime page
@@ -78,34 +67,15 @@ type HoppActionArgsMap = {
}
"modals.my.environment.edit": {
envName: string
variableName?: string
variableName: string
}
"modals.team.environment.edit": {
envName: string
variableName?: string
}
"modals.team.delete": {
teamId: string
}
"workspace.switch": {
teamId: string
variableName: string
}
"rest.request.open": {
doc: HoppRESTDocument
}
"request.save-as":
| {
requestType: "rest"
request: HoppRESTRequest
}
| {
requestType: "gql"
request: HoppGQLRequest
}
"request.open-tab": {
tab: RequestOptionTabs
}
"gql.request.open": {
request: HoppGQLRequest
}
@@ -174,7 +144,7 @@ type InvokeActionFunc = {
* @param args The argument passed to the action handler. Optional if action has no args required
*/
export const invokeAction: InvokeActionFunc = <
A extends HoppAction | HoppActionWithArgs,
A extends HoppAction | HoppActionWithArgs
>(
action: A,
args: ArgOfHoppAction<A>

View File

@@ -13,10 +13,9 @@ import {
Operation,
OperationResult,
Client,
AnyVariables,
} from "@urql/core"
import { AuthConfig, authExchange } from "@urql/exchange-auth"
// import { devtoolsExchange } from "@urql/devtools"
import { authExchange } from "@urql/exchange-auth"
import { devtoolsExchange } from "@urql/devtools"
import { SubscriptionClient } from "subscriptions-transport-ws"
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
@@ -68,43 +67,43 @@ const createSubscriptionClient = () => {
const createHoppClient = () => {
const exchanges = [
// devtoolsExchange,
devtoolsExchange,
dedupExchange,
authExchange(async (): Promise<AuthConfig> => {
const probableUser = platform.auth.getProbableUser()
if (probableUser !== null)
await platform.auth.waitProbableLoginToConfirm()
authExchange({
addAuthToOperation({ authState, operation }) {
if (!authState) {
return operation
}
return {
addAuthToOperation(operation) {
const fetchOptions =
typeof operation.context.fetchOptions === "function"
? operation.context.fetchOptions()
: operation.context.fetchOptions || {}
const fetchOptions =
typeof operation.context.fetchOptions === "function"
? operation.context.fetchOptions()
: operation.context.fetchOptions || {}
const authHeaders = platform.auth.getBackendHeaders()
const authHeaders = platform.auth.getBackendHeaders()
return makeOperation(operation.kind, operation, {
...operation.context,
fetchOptions: {
...fetchOptions,
headers: {
...fetchOptions.headers,
...authHeaders,
},
return makeOperation(operation.kind, operation, {
...operation.context,
fetchOptions: {
...fetchOptions,
headers: {
...fetchOptions.headers,
...authHeaders,
},
})
},
willAuthError() {
return platform.auth.willBackendHaveAuthError()
},
didAuthError() {
return false
},
async refreshAuth() {
// TODO
},
}
},
})
},
willAuthError() {
return platform.auth.willBackendHaveAuthError()
},
getAuth: async () => {
const probableUser = platform.auth.getProbableUser()
if (probableUser !== null)
await platform.auth.waitProbableLoginToConfirm()
return {}
},
}),
fetchExchange,
errorExchange({
@@ -166,9 +165,9 @@ export function initBackendGQLClient() {
})
}
type RunQueryOptions<T = any, V = AnyVariables> = {
type RunQueryOptions<T = any, V = object> = {
query: TypedDocumentNode<T, V>
variables: V
variables?: V
}
/**
@@ -184,11 +183,7 @@ export type GQLError<T extends string> =
error: T
}
export const runGQLQuery = <
DocType,
DocVarType extends AnyVariables,
DocErrorType extends string,
>(
export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
args: RunQueryOptions<DocType, DocVarType>
): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
const request = createRequest<DocType, DocVarType>(args.query, args.variables)
@@ -250,8 +245,8 @@ export const runGQLQuery = <
// Make sure to handle cases if the subscription fires with the same update multiple times
export const runGQLSubscription = <
DocType,
DocVarType extends AnyVariables,
DocErrorType extends string,
DocVarType,
DocErrorType extends string
>(
args: RunQueryOptions<DocType, DocVarType>
) => {
@@ -340,10 +335,10 @@ export const parseGQLErrorString = (s: string) =>
export const runMutation = <
DocType,
DocVariables extends object | undefined,
DocErrors extends string,
DocErrors extends string
>(
mutation: TypedDocumentNode<DocType, DocVariables>,
variables: DocVariables,
variables?: DocVariables,
additionalConfig?: Partial<OperationContext>
): TE.TaskEither<GQLError<DocErrors>, DocType> =>
pipe(

View File

@@ -73,22 +73,22 @@ describe("detect content type", () => {
// })
describe("text/html", () => {
// test("should return text/html for valid HTML data", () => {
// expect(
// detectContentType(`
// <!DOCTYPE html>
// <html>
// <head>
// <title>Page Title</title>
// </head>
// <body>
// <h1>This is a Heading</h1>
// <p>This is a paragraph.</p>
// </body>
// </html>
// `)
// ).toBe("text/html")
// })
test("should return text/html for valid HTML data", () => {
expect(
detectContentType(`
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>
<h1>This is a Heading</h1>
<p>This is a paragraph.</p>
</body>
</html>
`)
).toBe("text/html")
})
// TODO: Figure this test situation
// test("should return text/html for invalid HTML data", () => {

View File

@@ -126,7 +126,7 @@ const multipartFunctions = {
(nameArr) =>
[nameArr[1], pair[0].includes("filename") ? "" : pair[1]] as [
string,
string,
string
]
)
)

View File

@@ -71,9 +71,9 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const selectedEnvType = getSelectedEnvironmentType()
const envTypeIcon = `<span class="inline-flex -my-2 -mx-0.5 opacity-65 items-center text-base font-icon">${
selectedEnvType === "TEAM_ENV" ? "group" : "person"
}</span>`
const envTypeIcon = `<i class="inline-flex -my-1 -mx-0.5 opacity-65 items-center text-base material-icons border-secondary">${
selectedEnvType === "TEAM_ENV" ? "people" : "person"
}</i>`
const appendEditAction = (tooltip: HTMLElement) => {
const editIcon = document.createElement("span")
@@ -88,7 +88,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
variableName: parsedEnvKey,
})
})
editIcon.innerHTML = `<span class="inline-flex items-center px-1 -mx-1 -my-2 text-base font-icon">edit</span>`
editIcon.innerHTML = `<i class="inline-flex items-center px-1 -mx-1 -my-1 text-base material-icons border-secondary">drive_file_rename_outline</i>`
tooltip.appendChild(editIcon)
}

View File

@@ -8,7 +8,7 @@
*/
export const tupleToRecord = <
KeyType extends string | number | symbol,
ValueType,
ValueType
>(
tuples: [KeyType, ValueType][]
): Record<KeyType, ValueType> =>
@@ -25,7 +25,7 @@ export const tupleToRecord = <
*/
export const tupleWithSameKeysToRecord = <
KeyType extends string | number | symbol,
ValueType,
ValueType
>(
tuples: [KeyType, ValueType][]
): Record<KeyType, ValueType[]> => {

View File

@@ -195,7 +195,7 @@ const parseOpenAPIV3Body = (
// We only take the first definition
const [contentType, media]: [
string,
OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject,
OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
] = objs[0]
return contentType in knownContentTypes

View File

@@ -90,8 +90,7 @@ const getHoppReqParams = (item: Item): HoppRESTParam[] => {
}
type PMRequestAuthDef<
AuthType extends
RequestAuthDefinition["type"] = RequestAuthDefinition["type"],
AuthType extends RequestAuthDefinition["type"] = RequestAuthDefinition["type"]
> = AuthType extends RequestAuthDefinition["type"] & string
? // eslint-disable-next-line no-unused-vars
{ type: AuthType } & { [x in AuthType]: VariableDefinition[] }

View File

@@ -14,13 +14,7 @@ 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"
| "ctrl-alt"
| "ctrl-alt-shift"
type ModifierKeys = "ctrl" | "alt" | "ctrl-shift" | "alt-shift"
/* eslint-disable prettier/prettier */
// prettier-ignore
@@ -149,19 +143,18 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
}
function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
const modifierKeys = {
ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
alt: ev.altKey,
shift: ev.shiftKey,
}
const isShiftKey = ev.shiftKey
// 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("-")
// 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"
return activeModifier === "" ? null : (activeModifier as ModifierKeys)
// Test for Alt key
if (ev.altKey) return isShiftKey ? "alt-shift" : "alt"
return null
}
/**

Some files were not shown because too many files have changed in this diff Show More