Compare commits
56 Commits
feat/condi
...
pr/JoelJac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1afabec3a8 | ||
|
|
c9927ef032 | ||
|
|
d9bddb1094 | ||
|
|
14a3dad013 | ||
|
|
c683a5471c | ||
|
|
1b215be382 | ||
|
|
9e0453d3bb | ||
|
|
caf5d7ec0f | ||
|
|
6e7db67c9b | ||
|
|
27eb7690ba | ||
|
|
a875ab1ca9 | ||
|
|
d5e4211347 | ||
|
|
24f32d79b3 | ||
|
|
f48083ca7e | ||
|
|
c0ee3d02ae | ||
|
|
fb234cbf7e | ||
|
|
c19a8be6e9 | ||
|
|
b0e29b2e6c | ||
|
|
0962718b0c | ||
|
|
a08760351d | ||
|
|
55c191305c | ||
|
|
dfabf7e8bc | ||
|
|
834efce1c2 | ||
|
|
cdbc580ada | ||
|
|
008eb6b77b | ||
|
|
ac60843183 | ||
|
|
3c3fb1e4a9 | ||
|
|
88212e8cfe | ||
|
|
191fa376d2 | ||
|
|
6efae3a395 | ||
|
|
cb8678f07f | ||
|
|
b32b0f9bcb | ||
|
|
5a91fb53b2 | ||
|
|
b0b6edc58e | ||
|
|
8c57d81718 | ||
|
|
10bb68a538 | ||
|
|
d4d1e27ba9 | ||
|
|
d5c887f311 | ||
|
|
ce7adf6da3 | ||
|
|
c626fb9241 | ||
|
|
f21ed30e10 | ||
|
|
b55970cc7a | ||
|
|
74ad2e43a4 | ||
|
|
2d6282cf8b | ||
|
|
e255c46455 | ||
|
|
15c2c7bb5b | ||
|
|
71bcd22444 | ||
|
|
2d104160f2 | ||
|
|
f7c1825de5 | ||
|
|
2c1fd5d711 | ||
|
|
085fbb2a9b | ||
|
|
05f2d8817b | ||
|
|
81fbb22c51 | ||
|
|
01cf59c663 | ||
|
|
5c8ebaff3e | ||
|
|
0e70c28324 |
@@ -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"
|
||||
ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||
|
||||
# Google Auth Config
|
||||
GOOGLE_CLIENT_ID="************************************************"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
module.exports = {
|
||||
semi: false
|
||||
semi: false,
|
||||
trailingComma: "es5",
|
||||
singleQuote: false,
|
||||
printWidth: 80,
|
||||
useTabs: false,
|
||||
tabWidth: 2
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.2.0"
|
||||
"@codemirror/language": "^6.9.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@lezer/lr": "^1.3.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.1.0",
|
||||
"@lezer/generator": "^1.5.0",
|
||||
"mocha": "^9.2.2",
|
||||
"rollup": "^2.70.2",
|
||||
"rollup-plugin-dts": "^4.2.1",
|
||||
|
||||
@@ -107,7 +107,7 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Check to see if given auth provider is present in the ALLOWED_AUTH_PROVIDERS env variable
|
||||
* Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable
|
||||
*
|
||||
* @param provider Provider we want to check the presence of
|
||||
* @returns Boolean if provider specified is present or not
|
||||
@@ -117,8 +117,8 @@ export function authProviderCheck(provider: string) {
|
||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||
}
|
||||
|
||||
const envVariables = process.env.ALLOWED_AUTH_PROVIDERS
|
||||
? process.env.ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||
provider.trim().toUpperCase(),
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -29,22 +29,22 @@ export const JSON_INVALID = 'json_invalid';
|
||||
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
||||
|
||||
/**
|
||||
* Environment variable "ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
||||
*/
|
||||
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
|
||||
'"ALLOWED_AUTH_PROVIDERS" is not present in .env file';
|
||||
'"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file';
|
||||
|
||||
/**
|
||||
* Environment variable "ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
||||
*/
|
||||
export const ENV_EMPTY_AUTH_PROVIDERS =
|
||||
'"ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
||||
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
||||
|
||||
/**
|
||||
* Environment variable "ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
|
||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
|
||||
*/
|
||||
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
|
||||
'"ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
|
||||
'"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
|
||||
|
||||
/**
|
||||
* Tried to delete a user data document from fb firestore but failed.
|
||||
|
||||
@@ -9,7 +9,12 @@ 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';
|
||||
|
||||
/**
|
||||
@@ -156,21 +161,21 @@ export function isValidLength(title: string, length: number) {
|
||||
|
||||
/**
|
||||
* This function is called by bootstrap() in main.ts
|
||||
* It checks if the "ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||
* If not, it throws an error.
|
||||
*/
|
||||
export function checkEnvironmentAuthProvider() {
|
||||
if (!process.env.hasOwnProperty('ALLOWED_AUTH_PROVIDERS')) {
|
||||
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
||||
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
||||
}
|
||||
|
||||
if (process.env.ALLOWED_AUTH_PROVIDERS === '') {
|
||||
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||
}
|
||||
|
||||
const givenAuthProviders = process.env.ALLOWED_AUTH_PROVIDERS.split(',').map(
|
||||
(provider) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||
',',
|
||||
).map((provider) => provider.toLocaleUpperCase());
|
||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||
(provider: string) => provider.toLocaleUpperCase(),
|
||||
);
|
||||
|
||||
@@ -29,8 +29,18 @@ 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",
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
module.exports = {
|
||||
semi: false
|
||||
semi: false,
|
||||
trailingComma: "es5",
|
||||
singleQuote: false,
|
||||
printWidth: 80,
|
||||
useTabs: false,
|
||||
tabWidth: 2
|
||||
}
|
||||
|
||||
@@ -190,10 +190,11 @@ 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-56;
|
||||
@apply max-h-[45vh];
|
||||
@apply items-stretch;
|
||||
@apply overflow-y-auto;
|
||||
@apply text-secondary text-body;
|
||||
@@ -201,6 +202,10 @@ a {
|
||||
@apply leading-normal;
|
||||
@apply focus:outline-none;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
& > span {
|
||||
@apply block #{!important};
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
@@ -216,6 +221,7 @@ a {
|
||||
|
||||
[data-v-tippy] {
|
||||
@apply flex flex-1;
|
||||
@apply truncate;
|
||||
}
|
||||
|
||||
[interactive] > div {
|
||||
@@ -568,3 +574,11 @@ details[open] summary .indicator {
|
||||
@apply rounded;
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
.gql-operation-not-highlight {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.gql-operation-highlight {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@mixin base-theme {
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--font-mono: "Roboto Mono", monospace;
|
||||
--font-icon: "Material Icons";
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--font-icon: "Material Symbols Rounded Variable";
|
||||
--font-mono: "Roboto Mono Variable", monospace;
|
||||
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"open_workspace": "Open workspace",
|
||||
"paste": "Paste",
|
||||
"prettify": "Prettify",
|
||||
"rename": "Rename",
|
||||
"remove": "Remove",
|
||||
"restore": "Restore",
|
||||
"save": "Save",
|
||||
@@ -68,6 +69,8 @@
|
||||
"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",
|
||||
@@ -132,6 +135,7 @@
|
||||
"renamed": "Collection renamed",
|
||||
"request_in_use": "Request in use",
|
||||
"save_as": "Save as",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"select": "Select a Collection",
|
||||
"select_location": "Select location",
|
||||
"select_team": "Select a team",
|
||||
@@ -149,6 +153,8 @@
|
||||
"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_tab": "Are you sure you want to close 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": {
|
||||
@@ -189,6 +195,7 @@
|
||||
"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"
|
||||
@@ -199,18 +206,25 @@
|
||||
"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",
|
||||
@@ -240,6 +254,7 @@
|
||||
"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"
|
||||
@@ -267,6 +282,10 @@
|
||||
"graphql": {
|
||||
"mutations": "Mutations",
|
||||
"schema": "Schema",
|
||||
"switch_connection": "Switch connection",
|
||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||
"subscriptions": "Subscriptions"
|
||||
},
|
||||
"group": {
|
||||
@@ -296,6 +315,30 @@
|
||||
"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",
|
||||
@@ -432,8 +475,10 @@
|
||||
"payload": "Payload",
|
||||
"query": "Query",
|
||||
"raw_body": "Raw Request Body",
|
||||
"rename": "Rename Request",
|
||||
"renamed": "Request renamed",
|
||||
"run": "Run",
|
||||
"stop": "Stop",
|
||||
"save": "Save",
|
||||
"save_as": "Save as",
|
||||
"saved": "Request saved",
|
||||
@@ -473,9 +518,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",
|
||||
@@ -539,6 +584,10 @@
|
||||
"show_all": "Keyboard shortcuts",
|
||||
"title": "General"
|
||||
},
|
||||
"others": {
|
||||
"title": "Others",
|
||||
"prettify": "Prettify Editor's Content"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite people to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
@@ -559,6 +608,9 @@
|
||||
"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": "Generate code snippet",
|
||||
"method": "Method",
|
||||
"next_method": "Select Next method",
|
||||
"post_method": "Select POST method",
|
||||
@@ -567,6 +619,7 @@
|
||||
"reset_request": "Reset Request",
|
||||
"save_to_collections": "Save to Collections",
|
||||
"send_request": "Send Request",
|
||||
"save_request": "Save Request",
|
||||
"title": "Request"
|
||||
},
|
||||
"response": {
|
||||
@@ -575,10 +628,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"
|
||||
}
|
||||
},
|
||||
@@ -597,8 +650,79 @@
|
||||
"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 other tabs",
|
||||
"new_tab": "Open a new tab",
|
||||
"title": "Tabs"
|
||||
},
|
||||
"section": {
|
||||
"user": "User"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -658,8 +782,11 @@
|
||||
"tab": {
|
||||
"authorization": "Authorization",
|
||||
"body": "Body",
|
||||
"close": "Close Tab",
|
||||
"close_others": "Close other Tabs",
|
||||
"collections": "Collections",
|
||||
"documentation": "Documentation",
|
||||
"duplicate": "Duplicate Tab",
|
||||
"environments": "Environments",
|
||||
"headers": "Headers",
|
||||
"history": "History",
|
||||
|
||||
@@ -22,137 +22,140 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@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",
|
||||
"@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",
|
||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@hoppscotch/ui": "workspace:^",
|
||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@sentry/tracing": "^7.13.0",
|
||||
"@sentry/vue": "^7.13.0",
|
||||
"@urql/core": "^2.5.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@sentry/tracing": "^7.64.0",
|
||||
"@sentry/vue": "^7.64.0",
|
||||
"@urql/core": "^4.1.1",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
"@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",
|
||||
"@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",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"axios": "^0.21.4",
|
||||
"axios": "^1.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dioc": "workspace:^",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
"fp-ts": "^2.12.1",
|
||||
"fp-ts": "^2.16.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"globalthis": "^1.0.3",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql": "^16.8.0",
|
||||
"graphql-language-service-interface": "^2.9.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"httpsnippet": "^2.0.0",
|
||||
"insomnia-importers": "^3.3.0",
|
||||
"io-ts": "^2.2.16",
|
||||
"insomnia-importers": "^3.6.0",
|
||||
"io-ts": "^2.2.20",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath-plus": "^7.0.0",
|
||||
"jsonpath-plus": "^7.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lossless-json": "^2.0.8",
|
||||
"lossless-json": "^2.0.11",
|
||||
"minisearch": "^6.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"paho-mqtt": "^1.1.0",
|
||||
"path": "^0.12.7",
|
||||
"postman-collection": "^4.1.4",
|
||||
"postman-collection": "^4.2.0",
|
||||
"process": "^0.11.10",
|
||||
"qs": "^6.10.3",
|
||||
"rxjs": "^7.5.5",
|
||||
"qs": "^6.11.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"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.1",
|
||||
"splitpanes": "^3.1.5",
|
||||
"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.0",
|
||||
"util": "^0.12.4",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.2.25",
|
||||
"url": "^0.11.1",
|
||||
"util": "^0.12.5",
|
||||
"uuid": "^9.0.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-pdf-embed": "^1.1.4",
|
||||
"vue-router": "^4.0.16",
|
||||
"vue-tippy": "6.0.0-alpha.58",
|
||||
"vue-pdf-embed": "^1.1.6",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-tippy": "6.3.1",
|
||||
"vuedraggable-es": "^4.1.1",
|
||||
"wonka": "^4.0.15",
|
||||
"workbox-window": "^6.5.4",
|
||||
"xml-formatter": "^3.4.1",
|
||||
"wonka": "^6.3.4",
|
||||
"workbox-window": "^7.0.0",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/lossless-json": "^1.0.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/paho-mqtt": "^1.0.6",
|
||||
"@types/paho-mqtt": "^1.0.7",
|
||||
"@types/postman-collection": "^3.5.7",
|
||||
"@types/splitpanes": "^2.2.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@types/yargs-parser": "^21.0.0",
|
||||
"@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",
|
||||
"@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",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"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",
|
||||
"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",
|
||||
"windicss": "^3.5.6"
|
||||
}
|
||||
}
|
||||
|
||||
391
packages/hoppscotch-common/src/components.d.ts
vendored
391
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -1,181 +1,222 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
declare module "vue" {
|
||||
export interface GlobalComponents {
|
||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
|
||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
||||
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
||||
AppOptions: typeof import('./components/app/Options.vue')['default']
|
||||
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
|
||||
AppShare: typeof import('./components/app/Share.vue')['default']
|
||||
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
||||
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
||||
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
||||
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
|
||||
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
|
||||
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
|
||||
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
|
||||
AppSupport: typeof import('./components/app/Support.vue')['default']
|
||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
||||
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
||||
Collections: typeof import('./components/collections/index.vue')['default']
|
||||
CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
|
||||
CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
|
||||
CollectionsAddRequest: typeof import('./components/collections/AddRequest.vue')['default']
|
||||
CollectionsCollection: typeof import('./components/collections/Collection.vue')['default']
|
||||
CollectionsEdit: typeof import('./components/collections/Edit.vue')['default']
|
||||
CollectionsEditFolder: typeof import('./components/collections/EditFolder.vue')['default']
|
||||
CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default']
|
||||
CollectionsGraphql: typeof import('./components/collections/graphql/index.vue')['default']
|
||||
CollectionsGraphqlAdd: typeof import('./components/collections/graphql/Add.vue')['default']
|
||||
CollectionsGraphqlAddFolder: typeof import('./components/collections/graphql/AddFolder.vue')['default']
|
||||
CollectionsGraphqlAddRequest: typeof import('./components/collections/graphql/AddRequest.vue')['default']
|
||||
CollectionsGraphqlCollection: typeof import('./components/collections/graphql/Collection.vue')['default']
|
||||
CollectionsGraphqlEdit: typeof import('./components/collections/graphql/Edit.vue')['default']
|
||||
CollectionsGraphqlEditFolder: typeof import('./components/collections/graphql/EditFolder.vue')['default']
|
||||
CollectionsGraphqlEditRequest: typeof import('./components/collections/graphql/EditRequest.vue')['default']
|
||||
CollectionsGraphqlFolder: typeof import('./components/collections/graphql/Folder.vue')['default']
|
||||
CollectionsGraphqlImportExport: typeof import('./components/collections/graphql/ImportExport.vue')['default']
|
||||
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
|
||||
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
|
||||
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
|
||||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||
Environments: typeof import('./components/environments/index.vue')['default']
|
||||
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
||||
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
||||
EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default']
|
||||
EnvironmentsSelector: typeof import('./components/environments/Selector.vue')['default']
|
||||
EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
|
||||
EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']
|
||||
EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default']
|
||||
FirebaseLogin: typeof import('./components/firebase/Login.vue')['default']
|
||||
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
|
||||
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
|
||||
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
|
||||
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
|
||||
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
||||
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
||||
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
||||
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
||||
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
||||
History: typeof import('./components/history/index.vue')['default']
|
||||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
||||
HttpBody: typeof import('./components/http/Body.vue')['default']
|
||||
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
|
||||
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
|
||||
HttpHeaders: typeof import('./components/http/Headers.vue')['default']
|
||||
HttpImportCurl: typeof import('./components/http/ImportCurl.vue')['default']
|
||||
HttpOAuth2Authorization: typeof import('./components/http/OAuth2Authorization.vue')['default']
|
||||
HttpParameters: typeof import('./components/http/Parameters.vue')['default']
|
||||
HttpPreRequestScript: typeof import('./components/http/PreRequestScript.vue')['default']
|
||||
HttpRawBody: typeof import('./components/http/RawBody.vue')['default']
|
||||
HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
|
||||
HttpRequest: typeof import('./components/http/Request.vue')['default']
|
||||
HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
|
||||
HttpRequestTab: typeof import('./components/http/RequestTab.vue')['default']
|
||||
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
||||
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
||||
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
||||
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
|
||||
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
|
||||
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
|
||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||
LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
|
||||
LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
|
||||
LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
|
||||
LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
|
||||
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
|
||||
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
|
||||
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
||||
ProfileShortcodes: typeof import('./components/profile/Shortcodes.vue')['default']
|
||||
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
|
||||
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
|
||||
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
|
||||
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
||||
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
|
||||
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||
SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
|
||||
SmartCheckbox: typeof import('./../../hoppscotch-ui/src/components/smart/Checkbox.vue')['default']
|
||||
SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
|
||||
SmartConfirmModal: typeof import('./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue')['default']
|
||||
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
|
||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
||||
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
||||
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
|
||||
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
||||
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
||||
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
|
||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
||||
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
||||
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
||||
SmartTree: typeof import('./components/smart/Tree.vue')['default']
|
||||
SmartTreeBranch: typeof import('./components/smart/TreeBranch.vue')['default']
|
||||
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
||||
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
||||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
|
||||
Teams: typeof import('./components/teams/index.vue')['default']
|
||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
||||
TeamsEdit: typeof import('./components/teams/Edit.vue')['default']
|
||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
|
||||
TeamsMemberStack: typeof import('./components/teams/MemberStack.vue')['default']
|
||||
TeamsModal: typeof import('./components/teams/Modal.vue')['default']
|
||||
TeamsTeam: typeof import('./components/teams/Team.vue')['default']
|
||||
Tippy: typeof import('vue-tippy')['Tippy']
|
||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||
AppActionHandler: typeof import("./components/app/ActionHandler.vue")["default"]
|
||||
AppAnnouncement: typeof import("./components/app/Announcement.vue")["default"]
|
||||
AppContextMenu: typeof import("./components/app/ContextMenu.vue")["default"]
|
||||
AppDeveloperOptions: typeof import("./components/app/DeveloperOptions.vue")["default"]
|
||||
AppFooter: typeof import("./components/app/Footer.vue")["default"]
|
||||
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"]
|
||||
AppPaneLayout: typeof import("./components/app/PaneLayout.vue")["default"]
|
||||
AppShare: typeof import("./components/app/Share.vue")["default"]
|
||||
AppShortcuts: typeof import("./components/app/Shortcuts.vue")["default"]
|
||||
AppShortcutsEntry: typeof import("./components/app/ShortcutsEntry.vue")["default"]
|
||||
AppShortcutsPrompt: typeof import("./components/app/ShortcutsPrompt.vue")["default"]
|
||||
AppSidenav: typeof import("./components/app/Sidenav.vue")["default"]
|
||||
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"]
|
||||
Collections: typeof import("./components/collections/index.vue")["default"]
|
||||
CollectionsAdd: typeof import("./components/collections/Add.vue")["default"]
|
||||
CollectionsAddFolder: typeof import("./components/collections/AddFolder.vue")["default"]
|
||||
CollectionsAddRequest: typeof import("./components/collections/AddRequest.vue")["default"]
|
||||
CollectionsCollection: typeof import("./components/collections/Collection.vue")["default"]
|
||||
CollectionsEdit: typeof import("./components/collections/Edit.vue")["default"]
|
||||
CollectionsEditFolder: typeof import("./components/collections/EditFolder.vue")["default"]
|
||||
CollectionsEditRequest: typeof import("./components/collections/EditRequest.vue")["default"]
|
||||
CollectionsGraphql: typeof import("./components/collections/graphql/index.vue")["default"]
|
||||
CollectionsGraphqlAdd: typeof import("./components/collections/graphql/Add.vue")["default"]
|
||||
CollectionsGraphqlAddFolder: typeof import("./components/collections/graphql/AddFolder.vue")["default"]
|
||||
CollectionsGraphqlAddRequest: typeof import("./components/collections/graphql/AddRequest.vue")["default"]
|
||||
CollectionsGraphqlCollection: typeof import("./components/collections/graphql/Collection.vue")["default"]
|
||||
CollectionsGraphqlEdit: typeof import("./components/collections/graphql/Edit.vue")["default"]
|
||||
CollectionsGraphqlEditFolder: typeof import("./components/collections/graphql/EditFolder.vue")["default"]
|
||||
CollectionsGraphqlEditRequest: typeof import("./components/collections/graphql/EditRequest.vue")["default"]
|
||||
CollectionsGraphqlFolder: typeof import("./components/collections/graphql/Folder.vue")["default"]
|
||||
CollectionsGraphqlImportExport: typeof import("./components/collections/graphql/ImportExport.vue")["default"]
|
||||
CollectionsGraphqlRequest: typeof import("./components/collections/graphql/Request.vue")["default"]
|
||||
CollectionsImportExport: typeof import("./components/collections/ImportExport.vue")["default"]
|
||||
CollectionsMyCollections: typeof import("./components/collections/MyCollections.vue")["default"]
|
||||
CollectionsRequest: typeof import("./components/collections/Request.vue")["default"]
|
||||
CollectionsSaveRequest: typeof import("./components/collections/SaveRequest.vue")["default"]
|
||||
CollectionsTeamCollections: typeof import("./components/collections/TeamCollections.vue")["default"]
|
||||
Environments: typeof import("./components/environments/index.vue")["default"]
|
||||
EnvironmentsAdd: typeof import("./components/environments/Add.vue")["default"]
|
||||
EnvironmentsImportExport: typeof import("./components/environments/ImportExport.vue")["default"]
|
||||
EnvironmentsMy: typeof import("./components/environments/my/index.vue")["default"]
|
||||
EnvironmentsMyDetails: typeof import("./components/environments/my/Details.vue")["default"]
|
||||
EnvironmentsMyEnvironment: typeof import("./components/environments/my/Environment.vue")["default"]
|
||||
EnvironmentsSelector: typeof import("./components/environments/Selector.vue")["default"]
|
||||
EnvironmentsTeams: typeof import("./components/environments/teams/index.vue")["default"]
|
||||
EnvironmentsTeamsDetails: typeof import("./components/environments/teams/Details.vue")["default"]
|
||||
EnvironmentsTeamsEnvironment: typeof import("./components/environments/teams/Environment.vue")["default"]
|
||||
FirebaseLogin: typeof import("./components/firebase/Login.vue")["default"]
|
||||
FirebaseLogout: typeof import("./components/firebase/Logout.vue")["default"]
|
||||
GraphqlAuthorization: typeof import("./components/graphql/Authorization.vue")["default"]
|
||||
GraphqlField: typeof import("./components/graphql/Field.vue")["default"]
|
||||
GraphqlHeaders: typeof import("./components/graphql/Headers.vue")["default"]
|
||||
GraphqlQuery: typeof import("./components/graphql/Query.vue")["default"]
|
||||
GraphqlRequest: typeof import("./components/graphql/Request.vue")["default"]
|
||||
GraphqlRequestOptions: typeof import("./components/graphql/RequestOptions.vue")["default"]
|
||||
GraphqlRequestTab: typeof import("./components/graphql/RequestTab.vue")["default"]
|
||||
GraphqlResponse: typeof import("./components/graphql/Response.vue")["default"]
|
||||
GraphqlSidebar: typeof import("./components/graphql/Sidebar.vue")["default"]
|
||||
GraphqlSubscriptionLog: typeof import("./components/graphql/SubscriptionLog.vue")["default"]
|
||||
GraphqlType: typeof import("./components/graphql/Type.vue")["default"]
|
||||
GraphqlTypeLink: typeof import("./components/graphql/TypeLink.vue")["default"]
|
||||
GraphqlVariable: typeof import("./components/graphql/Variable.vue")["default"]
|
||||
History: typeof import("./components/history/index.vue")["default"]
|
||||
HistoryGraphqlCard: typeof import("./components/history/graphql/Card.vue")["default"]
|
||||
HistoryRestCard: typeof import("./components/history/rest/Card.vue")["default"]
|
||||
HoppButtonPrimary: typeof import("@hoppscotch/ui")["HoppButtonPrimary"]
|
||||
HoppButtonSecondary: typeof import("@hoppscotch/ui")["HoppButtonSecondary"]
|
||||
HoppSmartAnchor: typeof import("@hoppscotch/ui")["HoppSmartAnchor"]
|
||||
HoppSmartAutoComplete: typeof import("@hoppscotch/ui")["HoppSmartAutoComplete"]
|
||||
HoppSmartCheckbox: typeof import("@hoppscotch/ui")["HoppSmartCheckbox"]
|
||||
HoppSmartConfirmModal: typeof import("@hoppscotch/ui")["HoppSmartConfirmModal"]
|
||||
HoppSmartExpand: typeof import("@hoppscotch/ui")["HoppSmartExpand"]
|
||||
HoppSmartFileChip: typeof import("@hoppscotch/ui")["HoppSmartFileChip"]
|
||||
HoppSmartInput: typeof import("@hoppscotch/ui")["HoppSmartInput"]
|
||||
HoppSmartIntersection: typeof import("@hoppscotch/ui")["HoppSmartIntersection"]
|
||||
HoppSmartItem: typeof import("@hoppscotch/ui")["HoppSmartItem"]
|
||||
HoppSmartLink: typeof import("@hoppscotch/ui")["HoppSmartLink"]
|
||||
HoppSmartModal: typeof import("@hoppscotch/ui")["HoppSmartModal"]
|
||||
HoppSmartPicture: typeof import("@hoppscotch/ui")["HoppSmartPicture"]
|
||||
HoppSmartPlaceholder: typeof import("@hoppscotch/ui")["HoppSmartPlaceholder"]
|
||||
HoppSmartProgressRing: typeof import("@hoppscotch/ui")["HoppSmartProgressRing"]
|
||||
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"]
|
||||
HoppSmartTree: typeof import("@hoppscotch/ui")["HoppSmartTree"]
|
||||
HoppSmartWindow: typeof import("@hoppscotch/ui")["HoppSmartWindow"]
|
||||
HoppSmartWindows: typeof import("@hoppscotch/ui")["HoppSmartWindows"]
|
||||
HttpAuthorization: typeof import("./components/http/Authorization.vue")["default"]
|
||||
HttpAuthorizationApiKey: typeof import("./components/http/authorization/ApiKey.vue")["default"]
|
||||
HttpAuthorizationBasic: typeof import("./components/http/authorization/Basic.vue")["default"]
|
||||
HttpBody: typeof import("./components/http/Body.vue")["default"]
|
||||
HttpBodyParameters: typeof import("./components/http/BodyParameters.vue")["default"]
|
||||
HttpCodegenModal: typeof import("./components/http/CodegenModal.vue")["default"]
|
||||
HttpHeaders: typeof import("./components/http/Headers.vue")["default"]
|
||||
HttpImportCurl: typeof import("./components/http/ImportCurl.vue")["default"]
|
||||
HttpOAuth2Authorization: typeof import("./components/http/OAuth2Authorization.vue")["default"]
|
||||
HttpParameters: typeof import("./components/http/Parameters.vue")["default"]
|
||||
HttpPreRequestScript: typeof import("./components/http/PreRequestScript.vue")["default"]
|
||||
HttpRawBody: typeof import("./components/http/RawBody.vue")["default"]
|
||||
HttpReqChangeConfirmModal: typeof import("./components/http/ReqChangeConfirmModal.vue")["default"]
|
||||
HttpRequest: typeof import("./components/http/Request.vue")["default"]
|
||||
HttpRequestOptions: typeof import("./components/http/RequestOptions.vue")["default"]
|
||||
HttpRequestTab: typeof import("./components/http/RequestTab.vue")["default"]
|
||||
HttpResponse: typeof import("./components/http/Response.vue")["default"]
|
||||
HttpResponseMeta: typeof import("./components/http/ResponseMeta.vue")["default"]
|
||||
HttpSidebar: typeof import("./components/http/Sidebar.vue")["default"]
|
||||
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"]
|
||||
IconLucideCheckCircle: typeof import("~icons/lucide/check-circle")["default"]
|
||||
IconLucideChevronRight: typeof import("~icons/lucide/chevron-right")["default"]
|
||||
IconLucideGlobe: typeof import("~icons/lucide/globe")["default"]
|
||||
IconLucideHelpCircle: typeof import("~icons/lucide/help-circle")["default"]
|
||||
IconLucideInbox: typeof import("~icons/lucide/inbox")["default"]
|
||||
IconLucideInfo: typeof import("~icons/lucide/info")["default"]
|
||||
IconLucideLayers: typeof import("~icons/lucide/layers")["default"]
|
||||
IconLucideListEnd: typeof import("~icons/lucide/list-end")["default"]
|
||||
IconLucideMinus: typeof import("~icons/lucide/minus")["default"]
|
||||
IconLucideSearch: typeof import("~icons/lucide/search")["default"]
|
||||
IconLucideUsers: typeof import("~icons/lucide/users")["default"]
|
||||
IconLucideVerified: typeof import("~icons/lucide/verified")["default"]
|
||||
InterceptorsExtensionSubtitle: typeof import("./components/interceptors/ExtensionSubtitle.vue")["default"]
|
||||
LensesHeadersRenderer: typeof import("./components/lenses/HeadersRenderer.vue")["default"]
|
||||
LensesHeadersRendererEntry: typeof import("./components/lenses/HeadersRendererEntry.vue")["default"]
|
||||
LensesRenderersAudioLensRenderer: typeof import("./components/lenses/renderers/AudioLensRenderer.vue")["default"]
|
||||
LensesRenderersHTMLLensRenderer: typeof import("./components/lenses/renderers/HTMLLensRenderer.vue")["default"]
|
||||
LensesRenderersImageLensRenderer: typeof import("./components/lenses/renderers/ImageLensRenderer.vue")["default"]
|
||||
LensesRenderersJSONLensRenderer: typeof import("./components/lenses/renderers/JSONLensRenderer.vue")["default"]
|
||||
LensesRenderersPDFLensRenderer: typeof import("./components/lenses/renderers/PDFLensRenderer.vue")["default"]
|
||||
LensesRenderersRawLensRenderer: typeof import("./components/lenses/renderers/RawLensRenderer.vue")["default"]
|
||||
LensesRenderersVideoLensRenderer: typeof import("./components/lenses/renderers/VideoLensRenderer.vue")["default"]
|
||||
LensesRenderersXMLLensRenderer: typeof import("./components/lenses/renderers/XMLLensRenderer.vue")["default"]
|
||||
LensesResponseBodyRenderer: typeof import("./components/lenses/ResponseBodyRenderer.vue")["default"]
|
||||
ProfileShortcode: typeof import("./components/profile/Shortcode.vue")["default"]
|
||||
ProfileShortcodes: typeof import("./components/profile/Shortcodes.vue")["default"]
|
||||
ProfileUserDelete: typeof import("./components/profile/UserDelete.vue")["default"]
|
||||
RealtimeCommunication: typeof import("./components/realtime/Communication.vue")["default"]
|
||||
RealtimeConnectionConfig: typeof import("./components/realtime/ConnectionConfig.vue")["default"]
|
||||
RealtimeLog: typeof import("./components/realtime/Log.vue")["default"]
|
||||
RealtimeLogEntry: typeof import("./components/realtime/LogEntry.vue")["default"]
|
||||
RealtimeSubscription: typeof import("./components/realtime/Subscription.vue")["default"]
|
||||
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"]
|
||||
SmartChangeLanguage: typeof import("./components/smart/ChangeLanguage.vue")["default"]
|
||||
SmartCheckbox: typeof import("./../../hoppscotch-ui/src/components/smart/Checkbox.vue")["default"]
|
||||
SmartColorModePicker: typeof import("./components/smart/ColorModePicker.vue")["default"]
|
||||
SmartConfirmModal: typeof import("./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue")["default"]
|
||||
SmartEnvInput: typeof import("./components/smart/EnvInput.vue")["default"]
|
||||
SmartExpand: typeof import("./../../hoppscotch-ui/src/components/smart/Expand.vue")["default"]
|
||||
SmartFileChip: typeof import("./../../hoppscotch-ui/src/components/smart/FileChip.vue")["default"]
|
||||
SmartFontSizePicker: typeof import("./components/smart/FontSizePicker.vue")["default"]
|
||||
SmartInput: typeof import("./../../hoppscotch-ui/src/components/smart/Input.vue")["default"]
|
||||
SmartIntersection: typeof import("./../../hoppscotch-ui/src/components/smart/Intersection.vue")["default"]
|
||||
SmartItem: typeof import("./../../hoppscotch-ui/src/components/smart/Item.vue")["default"]
|
||||
SmartLink: typeof import("./../../hoppscotch-ui/src/components/smart/Link.vue")["default"]
|
||||
SmartModal: typeof import("./../../hoppscotch-ui/src/components/smart/Modal.vue")["default"]
|
||||
SmartPicture: typeof import("./../../hoppscotch-ui/src/components/smart/Picture.vue")["default"]
|
||||
SmartPlaceholder: typeof import("./../../hoppscotch-ui/src/components/smart/Placeholder.vue")["default"]
|
||||
SmartProgressRing: typeof import("./../../hoppscotch-ui/src/components/smart/ProgressRing.vue")["default"]
|
||||
SmartRadio: typeof import("./../../hoppscotch-ui/src/components/smart/Radio.vue")["default"]
|
||||
SmartRadioGroup: typeof import("./../../hoppscotch-ui/src/components/smart/RadioGroup.vue")["default"]
|
||||
SmartSlideOver: typeof import("./../../hoppscotch-ui/src/components/smart/SlideOver.vue")["default"]
|
||||
SmartSpinner: typeof import("./../../hoppscotch-ui/src/components/smart/Spinner.vue")["default"]
|
||||
SmartTab: typeof import("./../../hoppscotch-ui/src/components/smart/Tab.vue")["default"]
|
||||
SmartTabs: typeof import("./../../hoppscotch-ui/src/components/smart/Tabs.vue")["default"]
|
||||
SmartToggle: typeof import("./../../hoppscotch-ui/src/components/smart/Toggle.vue")["default"]
|
||||
SmartTree: typeof import("./../../hoppscotch-ui/src/components/smart/Tree.vue")["default"]
|
||||
SmartTreeBranch: typeof import("./../../hoppscotch-ui/src/components/smart/TreeBranch.vue")["default"]
|
||||
SmartWindow: typeof import("./../../hoppscotch-ui/src/components/smart/Window.vue")["default"]
|
||||
SmartWindows: typeof import("./../../hoppscotch-ui/src/components/smart/Windows.vue")["default"]
|
||||
TabPrimary: typeof import("./components/tab/Primary.vue")["default"]
|
||||
TabSecondary: typeof import("./components/tab/Secondary.vue")["default"]
|
||||
Teams: typeof import("./components/teams/index.vue")["default"]
|
||||
TeamsAdd: typeof import("./components/teams/Add.vue")["default"]
|
||||
TeamsEdit: typeof import("./components/teams/Edit.vue")["default"]
|
||||
TeamsInvite: typeof import("./components/teams/Invite.vue")["default"]
|
||||
TeamsMemberStack: typeof import("./components/teams/MemberStack.vue")["default"]
|
||||
TeamsModal: typeof import("./components/teams/Modal.vue")["default"]
|
||||
TeamsTeam: typeof import("./components/teams/Team.vue")["default"]
|
||||
Tippy: typeof import("vue-tippy")["Tippy"]
|
||||
WorkspaceCurrent: typeof import("./components/workspace/Current.vue")["default"]
|
||||
WorkspaceSelector: typeof import("./components/workspace/Selector.vue")["default"]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,57 @@
|
||||
<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 { defineActionHandler } from "~/helpers/actions"
|
||||
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()
|
||||
|
||||
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
|
||||
})
|
||||
@@ -20,7 +60,20 @@ 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>
|
||||
|
||||
@@ -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 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"
|
||||
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"
|
||||
@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">
|
||||
<span class="flex space-x-1">
|
||||
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
|
||||
<kbd class="shortcut-key">K</kbd>
|
||||
</span>
|
||||
@@ -254,8 +254,10 @@ import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { useToast } from "~/composables/toast"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
/**
|
||||
* Once the PWA code is initialized, this holds a method
|
||||
@@ -372,6 +374,8 @@ const handleTeamEdit = () => {
|
||||
editingTeamID.value = workspace.value.teamID
|
||||
editingTeamName.value = { name: selectedTeam.value.name }
|
||||
displayModalEdit(true)
|
||||
} else {
|
||||
noPermission()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,6 +386,19 @@ const settings = ref<any | null>(null)
|
||||
const logout = ref<any | null>(null)
|
||||
const accountActions = ref<any | null>(null)
|
||||
|
||||
defineActionHandler("modals.team.edit", handleTeamEdit)
|
||||
|
||||
defineActionHandler("modals.team.invite", () => {
|
||||
if (
|
||||
selectedTeam.value?.myRole === "OWNER" ||
|
||||
selectedTeam.value?.myRole === "EDITOR"
|
||||
) {
|
||||
inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
|
||||
} else {
|
||||
noPermission()
|
||||
}
|
||||
})
|
||||
|
||||
defineActionHandler(
|
||||
"user.login",
|
||||
() => {
|
||||
@@ -389,4 +406,8 @@ defineActionHandler(
|
||||
},
|
||||
computed(() => !currentUser.value)
|
||||
)
|
||||
|
||||
const noPermission = () => {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
112
packages/hoppscotch-common/src/components/app/Inspection.vue
Normal file
112
packages/hoppscotch-common/src/components/app/Inspection.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<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>
|
||||
@@ -8,91 +8,41 @@
|
||||
{{ t("settings.interceptor_description") }}
|
||||
</p>
|
||||
</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
|
||||
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>
|
||||
</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 { useReadonlyStream } from "@composables/stream"
|
||||
import { extensionStatus$ } from "~/newstore/HoppExtension"
|
||||
import { useService } from "dioc/vue"
|
||||
import { Ref, unref } from "vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
|
||||
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
|
||||
const interceptorService = useService(InterceptorService)
|
||||
|
||||
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
|
||||
const interceptorSelection =
|
||||
interceptorService.currentInterceptorID as Ref<string>
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
})
|
||||
const interceptors = interceptorService.availableInterceptors
|
||||
</script>
|
||||
|
||||
@@ -130,13 +130,12 @@
|
||||
@click="nativeShare()"
|
||||
/>
|
||||
</div>
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { watch } from "vue"
|
||||
import IconSidebar from "~icons/lucide/sidebar"
|
||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
||||
import IconBook from "~icons/lucide/book"
|
||||
@@ -151,13 +150,12 @@ 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 { defineActionHandler } from "~/helpers/actions"
|
||||
import { invokeAction } 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")
|
||||
@@ -174,10 +172,6 @@ defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
defineActionHandler("modals.share.toggle", () => {
|
||||
showShare.value = !showShare.value
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
@@ -198,7 +192,7 @@ const expandCollection = () => {
|
||||
}
|
||||
|
||||
const expandInvite = () => {
|
||||
showShare.value = true
|
||||
invokeAction("modals.share.toggle")
|
||||
}
|
||||
|
||||
const nativeShare = () => {
|
||||
|
||||
@@ -18,13 +18,18 @@
|
||||
:horizontal="COLUMN_LAYOUT"
|
||||
@resize="setPaneEvent($event, 'horizontal')"
|
||||
>
|
||||
<Pane :size="PANE_MAIN_TOP_SIZE" class="flex flex-col !overflow-auto">
|
||||
<Pane
|
||||
:size="PANE_MAIN_TOP_SIZE"
|
||||
class="flex flex-col !overflow-auto"
|
||||
min-size="25"
|
||||
>
|
||||
<slot name="primary" />
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="hasSecondary"
|
||||
:size="PANE_MAIN_BOTTOM_SIZE"
|
||||
class="flex flex-col !overflow-auto"
|
||||
min-size="25"
|
||||
>
|
||||
<slot name="secondary" />
|
||||
</Pane>
|
||||
@@ -33,7 +38,7 @@
|
||||
<Pane
|
||||
v-if="SIDEBAR && hasSidebar"
|
||||
:size="PANE_SIDEBAR_SIZE"
|
||||
min-size="20"
|
||||
min-size="25"
|
||||
class="flex flex-col !overflow-auto bg-primaryContrast"
|
||||
>
|
||||
<slot name="sidebar" />
|
||||
@@ -78,10 +83,10 @@ type PaneEvent = {
|
||||
size: number
|
||||
}
|
||||
|
||||
const PANE_MAIN_SIZE = ref(74)
|
||||
const PANE_SIDEBAR_SIZE = ref(26)
|
||||
const PANE_MAIN_TOP_SIZE = ref(42)
|
||||
const PANE_MAIN_BOTTOM_SIZE = ref(58)
|
||||
const PANE_MAIN_SIZE = ref(70)
|
||||
const PANE_SIDEBAR_SIZE = ref(30)
|
||||
const PANE_MAIN_TOP_SIZE = ref(35)
|
||||
const PANE_MAIN_BOTTOM_SIZE = ref(65)
|
||||
|
||||
if (!COLUMN_LAYOUT.value) {
|
||||
PANE_MAIN_TOP_SIZE.value = 50
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
||||
>
|
||||
<div class="flex flex-col px-6 py-4 border-b border-dividerLight">
|
||||
<input
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
/>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
styles="px-6 py-4 border-b border-dividerLight"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
input-styles="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col divide-y divide-dividerLight">
|
||||
<HoppSmartPlaceholder
|
||||
@@ -21,6 +19,7 @@
|
||||
>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
</HoppSmartPlaceholder>
|
||||
|
||||
<details
|
||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||
v-else
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
||||
<div class="flex pb-4 my-4 space-x-2">
|
||||
<div class="flex mb-4 space-x-2">
|
||||
<div class="flex flex-col items-end space-y-4 text-right">
|
||||
<span class="flex items-center flex-1">
|
||||
{{ t("shortcut.request.send_request") }}
|
||||
|
||||
135
packages/hoppscotch-common/src/components/app/Social.vue
Normal file
135
packages/hoppscotch-common/src/components/app/Social.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<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>
|
||||
@@ -80,10 +80,11 @@ 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<{
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<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>
|
||||
@@ -0,0 +1,71 @@
|
||||
<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>
|
||||
@@ -95,6 +95,22 @@ 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()
|
||||
|
||||
@@ -110,6 +126,18 @@ 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("")
|
||||
|
||||
@@ -236,3 +264,4 @@ function newUseArrowKeysForNavigation() {
|
||||
return { selectedEntry }
|
||||
}
|
||||
</script>
|
||||
~/services/spotlight/searchers/workspace.searcher
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelAdd"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addNewCollection"
|
||||
/>
|
||||
<label for="selectLabelAdd">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="addNewCollection"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -65,28 +57,28 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const name = ref("")
|
||||
const editingName = ref("")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (!show) {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addNewCollection = () => {
|
||||
if (!name.value) {
|
||||
if (!editingName.value) {
|
||||
toast.error(t("collection.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", name.value)
|
||||
emit("submit", editingName.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelAddFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addFolder"
|
||||
/>
|
||||
<label for="selectLabelAddFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
input-styles="floating-input"
|
||||
:label="t('action.label')"
|
||||
@submit="addFolder"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -65,27 +57,27 @@ const emit = defineEmits<{
|
||||
(e: "add-folder", name: string): void
|
||||
}>()
|
||||
|
||||
const name = ref("")
|
||||
const editingName = ref("")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (!show) {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addFolder = () => {
|
||||
if (name.value.trim() === "") {
|
||||
if (editingName.value.trim() === "") {
|
||||
toast.error(t("folder.invalid_name"))
|
||||
return
|
||||
}
|
||||
emit("add-folder", name.value)
|
||||
emit("add-folder", editingName.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,19 +6,13 @@
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelAddRequest"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addRequest"
|
||||
/>
|
||||
<label for="selectLabelAddRequest">{{ t("action.label") }}</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="addRequest"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -64,23 +58,23 @@ const emit = defineEmits<{
|
||||
(event: "add-request", name: string): void
|
||||
}>()
|
||||
|
||||
const name = ref("")
|
||||
const editingName = ref("")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
name.value = currentActiveTab.value.document.request.name
|
||||
editingName.value = currentActiveTab.value.document.request.name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addRequest = () => {
|
||||
if (name.value.trim() === "") {
|
||||
if (editingName.value.trim() === "") {
|
||||
toast.error(`${t("error.empty_req_name")}`)
|
||||
return
|
||||
}
|
||||
emit("add-request", name.value)
|
||||
emit("add-request", editingName.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveCollection"
|
||||
/>
|
||||
<label for="selectLabelEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
input-styles="floating-input"
|
||||
:label="t('action.label')"
|
||||
@submit="saveCollection"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -67,26 +59,26 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const name = ref("")
|
||||
const editingName = ref("")
|
||||
|
||||
watch(
|
||||
() => props.editingCollectionName,
|
||||
(newName) => {
|
||||
name.value = newName
|
||||
editingName.value = newName
|
||||
}
|
||||
)
|
||||
|
||||
const saveCollection = () => {
|
||||
if (name.value.trim() === "") {
|
||||
if (editingName.value.trim() === "") {
|
||||
toast.error(t("collection.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", name.value)
|
||||
emit("submit", editingName.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelEditFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="editFolder"
|
||||
/>
|
||||
<label for="selectLabelEditFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="editFolder"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -67,26 +59,26 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const name = ref("")
|
||||
const editingName = ref("")
|
||||
|
||||
watch(
|
||||
() => props.editingFolderName,
|
||||
(newName) => {
|
||||
name.value = newName
|
||||
editingName.value = newName
|
||||
}
|
||||
)
|
||||
|
||||
const editFolder = () => {
|
||||
if (name.value.trim() === "") {
|
||||
if (editingName.value.trim() === "") {
|
||||
toast.error(t("folder.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", name.value)
|
||||
emit("submit", editingName.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelEditReq"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="editRequest"
|
||||
/>
|
||||
<label for="selectLabelEditReq">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="editRequest"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -68,19 +60,19 @@ const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void
|
||||
}>()
|
||||
|
||||
const name = useVModel(props, "modelValue")
|
||||
const editingName = useVModel(props, "modelValue")
|
||||
|
||||
const editRequest = () => {
|
||||
if (name.value.trim() === "") {
|
||||
if (editingName.value.trim() === "") {
|
||||
toast.error(t("request.invalid_name"))
|
||||
return
|
||||
}
|
||||
|
||||
emit("submit", name.value)
|
||||
emit("submit", editingName.value)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<SmartTree :adapter="myAdapter">
|
||||
<HoppSmartTree :adapter="myAdapter">
|
||||
<template
|
||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
@@ -291,7 +291,7 @@
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</template>
|
||||
</SmartTree>
|
||||
</HoppSmartTree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -303,7 +303,10 @@ 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 "~/helpers/treeAdapter"
|
||||
import {
|
||||
ChildrenResult,
|
||||
SmartTreeAdapter,
|
||||
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
@@ -8,21 +8,15 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<div class="relative flex">
|
||||
<input
|
||||
id="selectLabelSaveReq"
|
||||
v-model="requestName"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveRequestAs"
|
||||
/>
|
||||
<label for="selectLabelSaveReq">
|
||||
{{ t("request.name") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="requestName"
|
||||
styles="relative flex"
|
||||
placeholder=" "
|
||||
:label="t('request.name')"
|
||||
input-styles="floating-input"
|
||||
@submit="saveRequestAs"
|
||||
/>
|
||||
|
||||
<label class="p-4">
|
||||
{{ t("collection.select_location") }}
|
||||
</label>
|
||||
@@ -62,7 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref, watch } from "vue"
|
||||
import { computed, nextTick, reactive, ref, watch } from "vue"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import {
|
||||
HoppGQLRequest,
|
||||
@@ -77,7 +71,6 @@ import {
|
||||
updateTeamRequest,
|
||||
} from "~/helpers/backend/mutations/TeamRequest"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import {
|
||||
@@ -88,8 +81,9 @@ import {
|
||||
} from "~/newstore/collections"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { platform } from "~/platform"
|
||||
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
|
||||
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -107,10 +101,12 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
mode: "rest" | "graphql"
|
||||
request?: HoppRESTRequest | HoppGQLRequest | null
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
mode: "rest",
|
||||
request: null,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -126,22 +122,36 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const gqlRequestName = useGQLRequestName()
|
||||
const restRequestName = computedWithControl(
|
||||
() => currentActiveTab.value,
|
||||
() => currentActiveTab.value.document.request.name
|
||||
const gqlRequestName = computedWithControl(
|
||||
() => activeGQLTab.value,
|
||||
() => activeGQLTab.value.document.request.name
|
||||
)
|
||||
|
||||
const requestName = ref(
|
||||
props.mode === "rest" ? restRequestName.value : gqlRequestName.value
|
||||
const restRequestName = computedWithControl(
|
||||
() => activeRESTTab.value,
|
||||
() => activeRESTTab.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)
|
||||
|
||||
watch(
|
||||
() => [currentActiveTab.value, gqlRequestName.value],
|
||||
() => [activeRESTTab.value, activeGQLTab.value],
|
||||
() => {
|
||||
if (props.mode === "rest") {
|
||||
requestName.value = currentActiveTab.value?.document.request.name ?? ""
|
||||
} else requestName.value = gqlRequestName.value
|
||||
requestName.value = activeRESTTab.value?.document.request.name ?? ""
|
||||
} else {
|
||||
requestName.value = activeGQLTab.value?.document.request.name ?? ""
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -200,8 +210,8 @@ const saveRequestAs = async () => {
|
||||
|
||||
const requestUpdated =
|
||||
props.mode === "rest"
|
||||
? cloneDeep(currentActiveTab.value.document.request)
|
||||
: cloneDeep(getGQLSession().request)
|
||||
? cloneDeep(activeRESTTab.value.document.request)
|
||||
: cloneDeep(activeGQLTab.value.document.request)
|
||||
|
||||
requestUpdated.name = requestName.value
|
||||
|
||||
@@ -214,7 +224,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
currentActiveTab.value.document = {
|
||||
activeRESTTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -241,7 +251,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
currentActiveTab.value.document = {
|
||||
activeRESTTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -269,7 +279,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
currentActiveTab.value.document = {
|
||||
activeRESTTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -429,7 +439,7 @@ const updateTeamCollectionOrFolder = (
|
||||
(result) => {
|
||||
const { createRequestInCollection } = result
|
||||
|
||||
currentActiveTab.value.document = {
|
||||
activeRESTTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -450,7 +460,7 @@ const updateTeamCollectionOrFolder = (
|
||||
const requestSaved = () => {
|
||||
toast.success(`${t("request.added")}`)
|
||||
nextTick(() => {
|
||||
currentActiveTab.value.document.isDirty = false
|
||||
activeRESTTab.value.document.isDirty = false
|
||||
})
|
||||
hideModal()
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<SmartTree :adapter="teamAdapter">
|
||||
<HoppSmartTree :adapter="teamAdapter">
|
||||
<template
|
||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
@@ -311,7 +311,7 @@
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</template>
|
||||
</SmartTree>
|
||||
</HoppSmartTree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -326,7 +326,10 @@ 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 "~/helpers/treeAdapter"
|
||||
import {
|
||||
ChildrenResult,
|
||||
SmartTreeAdapter,
|
||||
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlAdd"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addNewCollection"
|
||||
/>
|
||||
<label for="selectLabelGqlAdd">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="name"
|
||||
placeholder=" "
|
||||
input-styles="floating-input"
|
||||
:label="t('action.label')"
|
||||
@submit="addNewCollection"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlAddFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addFolder"
|
||||
/>
|
||||
<label for="selectLabelGqlAddFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="name"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="addFolder"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlAddRequest"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addRequest"
|
||||
/>
|
||||
<label for="selectLabelGqlAddRequest">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="addRequest"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -44,7 +36,7 @@
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { getGQLSession } from "~/newstore/GQLSession"
|
||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -65,24 +57,24 @@ const emit = defineEmits<{
|
||||
): void
|
||||
}>()
|
||||
|
||||
const name = ref("")
|
||||
const editingName = ref("")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
name.value = getGQLSession().request.name
|
||||
editingName.value = currentActiveTab.value?.document.request.name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addRequest = () => {
|
||||
if (!name.value) {
|
||||
if (!editingName.value) {
|
||||
toast.error(`${t("error.empty_req_name")}`)
|
||||
return
|
||||
}
|
||||
emit("add-request", {
|
||||
name: name.value,
|
||||
name: editingName.value,
|
||||
path: props.folderPath,
|
||||
})
|
||||
hideModal()
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveCollection"
|
||||
/>
|
||||
<label for="selectLabelGqlEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="saveCollection"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -60,17 +52,17 @@ const emit = defineEmits<{
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const name = ref<string | null>()
|
||||
const editingName = ref<string | null>()
|
||||
|
||||
watch(
|
||||
() => props.editingCollectionName,
|
||||
(val) => {
|
||||
name.value = val
|
||||
editingName.value = val
|
||||
}
|
||||
)
|
||||
|
||||
const saveCollection = () => {
|
||||
if (!name.value) {
|
||||
if (!editingName.value) {
|
||||
toast.error(`${t("collection.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
@@ -78,7 +70,7 @@ const saveCollection = () => {
|
||||
// TODO: Better typechecking here ?
|
||||
const collectionUpdated = {
|
||||
...(props.editingCollection as any),
|
||||
name: name.value,
|
||||
name: editingName.value,
|
||||
}
|
||||
|
||||
editGraphqlCollection(props.editingCollectionIndex, collectionUpdated)
|
||||
@@ -86,7 +78,7 @@ const saveCollection = () => {
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = null
|
||||
editingName.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="$emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlEditFolder"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="editFolder"
|
||||
/>
|
||||
<label for="selectLabelGqlEditFolder">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="name"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="editFolder"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelGqlEditReq"
|
||||
v-model="requestUpdateData.name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveRequest"
|
||||
/>
|
||||
<label for="selectLabelGqlEditReq">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="requestUpdateData.name"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="saveRequest"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
|
||||
@@ -132,7 +132,7 @@ import { useToast } from "@composables/toast"
|
||||
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { removeGraphqlRequest } from "~/newstore/collections"
|
||||
import { setGQLSession } from "~/newstore/GQLSession"
|
||||
import { createNewTab } from "~/helpers/graphql/tab"
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
@@ -179,7 +179,7 @@ const selectRequest = () => {
|
||||
if (props.saveRequest) {
|
||||
pick()
|
||||
} else {
|
||||
setGQLSession({
|
||||
createNewTab({
|
||||
request: cloneDeep(
|
||||
makeGQLRequest({
|
||||
name: props.request.name,
|
||||
@@ -190,8 +190,7 @@ const selectRequest = () => {
|
||||
auth: props.request.auth,
|
||||
})
|
||||
),
|
||||
schema: "",
|
||||
response: "",
|
||||
isDirty: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="t('action.search')"
|
||||
class="py-2 pl-4 pr-2 bg-transparent"
|
||||
class="py-2 pl-4 pr-2 bg-transparent !border-0"
|
||||
/>
|
||||
<div
|
||||
class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"
|
||||
@@ -137,7 +137,6 @@ import {
|
||||
addGraphqlFolder,
|
||||
saveGraphqlRequestAs,
|
||||
} from "~/newstore/collections"
|
||||
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
|
||||
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
@@ -146,6 +145,7 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { platform } from "~/platform"
|
||||
import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -267,15 +267,15 @@ export default defineComponent({
|
||||
},
|
||||
onAddRequest({ name, path }) {
|
||||
const newRequest = {
|
||||
...getGQLSession().request,
|
||||
...currentActiveTab.value.document.request,
|
||||
name,
|
||||
}
|
||||
|
||||
saveGraphqlRequestAs(path, newRequest)
|
||||
setGQLSession({
|
||||
|
||||
createNewTab({
|
||||
request: newRequest,
|
||||
schema: "",
|
||||
response: "",
|
||||
isDirty: false,
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
"
|
||||
>
|
||||
<WorkspaceCurrent :section="t('tab.collections')" />
|
||||
<input
|
||||
|
||||
<HoppSmartInput
|
||||
v-model="filterTexts"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="t('action.search')"
|
||||
class="py-2 pl-4 pr-2 bg-transparent"
|
||||
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
|
||||
type="search"
|
||||
:autofocus="false"
|
||||
:disabled="collectionsType.type === 'team-collections'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
t("environment.name")
|
||||
}}</label>
|
||||
<input
|
||||
v-model="name"
|
||||
v-model="editingName"
|
||||
type="text"
|
||||
:placeholder="t('environment.variable')"
|
||||
class="input"
|
||||
@@ -88,7 +88,6 @@ const props = defineProps<{
|
||||
position: { top: number; left: number }
|
||||
name: string
|
||||
value: string
|
||||
replaceWithVariable: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -106,7 +105,7 @@ watch(
|
||||
scope.value = {
|
||||
type: "global",
|
||||
}
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
replaceWithVariable.value = false
|
||||
}
|
||||
}
|
||||
@@ -132,22 +131,22 @@ const scope = ref<Scope>({
|
||||
|
||||
const replaceWithVariable = ref(false)
|
||||
|
||||
const name = ref("")
|
||||
const editingName = ref(props.name)
|
||||
|
||||
const addEnvironment = async () => {
|
||||
if (!name.value) {
|
||||
if (!editingName.value) {
|
||||
toast.error(`${t("environment.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
if (scope.value.type === "global") {
|
||||
addGlobalEnvVariable({
|
||||
key: name.value,
|
||||
key: editingName.value,
|
||||
value: props.value,
|
||||
})
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
} else if (scope.value.type === "my-environment") {
|
||||
addEnvironmentVariable(scope.value.index, {
|
||||
key: name.value,
|
||||
key: editingName.value,
|
||||
value: props.value,
|
||||
})
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
@@ -155,7 +154,7 @@ const addEnvironment = async () => {
|
||||
const newVariables = [
|
||||
...scope.value.environment.environment.variables,
|
||||
{
|
||||
key: name.value,
|
||||
key: editingName.value,
|
||||
value: props.value,
|
||||
},
|
||||
]
|
||||
@@ -179,7 +178,7 @@ const addEnvironment = async () => {
|
||||
}
|
||||
if (replaceWithVariable.value) {
|
||||
//replace the current tab endpoint with the variable name with << and >>
|
||||
const variableName = `<<${name.value}>>`
|
||||
const variableName = `<<${editingName.value}>>`
|
||||
//replace the currenttab endpoint containing the value in the text with variablename
|
||||
currentActiveTab.value.document.request.endpoint =
|
||||
currentActiveTab.value.document.request.endpoint.replace(
|
||||
|
||||
@@ -1,203 +1,305 @@
|
||||
<template>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="bg-transparent select-wrapper"
|
||||
<div class="flex divide-x divide-dividerLight">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => envSelectorActions!.focus()"
|
||||
>
|
||||
<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="tippyActions"
|
||||
role="menu"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="select-wrapper"
|
||||
>
|
||||
<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()
|
||||
}
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLayers"
|
||||
:label="
|
||||
mdAndLarger
|
||||
? selectedEnv.type !== 'NO_ENV_SELECTED'
|
||||
? selectedEnv.name
|
||||
: `${t('environment.select')}`
|
||||
: ''
|
||||
"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<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
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="envSelectorActions"
|
||||
role="menu"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartTab
|
||||
:id="'my-environments'"
|
||||
:label="`${t('environment.my_environments')}`"
|
||||
<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
|
||||
>
|
||||
<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')"
|
||||
<HoppSmartTab
|
||||
:id="'my-environments'"
|
||||
:label="`${t('environment.my_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}`"
|
||||
v-for="(gen, index) in myEnvironments"
|
||||
:key="`gen-${index}`"
|
||||
:icon="IconLayers"
|
||||
:label="gen.environment.name"
|
||||
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
|
||||
:active-info-icon="isEnvActive(gen.id)"
|
||||
:label="gen.name"
|
||||
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
|
||||
:active-info-icon="index === selectedEnv.index"
|
||||
@click="
|
||||
() => {
|
||||
handleEnvironmentChange(index, {
|
||||
type: 'team-environment',
|
||||
environment: gen,
|
||||
})
|
||||
selectedEnvironmentIndex = {
|
||||
type: 'MY_ENV',
|
||||
index: index,
|
||||
}
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="teamEnvironmentList.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
<div
|
||||
v-if="myEnvironments.length === 0"
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<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
|
||||
v-if="!teamListLoading && teamAdapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
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,
|
||||
}"
|
||||
>
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(teamAdapterError) }}
|
||||
{{ t("environment.list") }}
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:disabled="!selectedEnv.variables"
|
||||
:title="t('action.edit')"
|
||||
:icon="IconEdit"
|
||||
@click="
|
||||
() => {
|
||||
editEnv()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from "vue"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconLayers from "~icons/lucide/layers"
|
||||
import IconGlobe from "~icons/lucide/globe"
|
||||
import IconEye from "~icons/lucide/eye"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
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 { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||
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
|
||||
}>()
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const mdAndLarger = breakpoints.greater("md")
|
||||
@@ -212,39 +314,6 @@ 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)
|
||||
@@ -279,157 +348,41 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const handleEnvironmentChange = (
|
||||
index: number,
|
||||
env?:
|
||||
| {
|
||||
type: "my-environment"
|
||||
environment: Environment
|
||||
}
|
||||
| {
|
||||
type: "team-environment"
|
||||
environment: TeamEnvironment
|
||||
}
|
||||
) => {
|
||||
if (props.isScopeSelector && env) {
|
||||
if (env.type === "my-environment") {
|
||||
emit("update:modelValue", {
|
||||
type: "my-environment",
|
||||
environment: env.environment,
|
||||
index,
|
||||
})
|
||||
} else if (env.type === "team-environment") {
|
||||
emit("update:modelValue", {
|
||||
type: "team-environment",
|
||||
environment: env.environment,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (env && env.type === "my-environment") {
|
||||
selectedEnvironmentIndex.value = {
|
||||
type: "MY_ENV",
|
||||
index,
|
||||
}
|
||||
} else if (env && env.type === "team-environment") {
|
||||
selectedEnvironmentIndex.value = {
|
||||
type: "TEAM_ENV",
|
||||
teamEnvID: env.environment.id,
|
||||
teamID: env.environment.teamID,
|
||||
environment: env.environment.environment,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isEnvActive = (id: string | number) => {
|
||||
if (props.isScopeSelector) {
|
||||
if (props.modelValue?.type === "my-environment") {
|
||||
return props.modelValue.index === id
|
||||
} else if (props.modelValue?.type === "team-environment") {
|
||||
return (
|
||||
props.modelValue?.type === "team-environment" &&
|
||||
props.modelValue.environment &&
|
||||
props.modelValue.environment.id === id
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||
return selectedEnv.value.index === id
|
||||
} else {
|
||||
return (
|
||||
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||
selectedEnv.value.teamEnvID === id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedEnv = computed(() => {
|
||||
if (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") {
|
||||
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,
|
||||
}
|
||||
} 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: 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" }
|
||||
name: teamEnv.environment.name,
|
||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||
variables: teamEnv.environment.variables,
|
||||
}
|
||||
} 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",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
})
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const envSelectorActions = ref<TippyComponent | null>(null)
|
||||
const envQuickPeekActions = ref<TippyComponent | null>(null)
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
@@ -443,4 +396,32 @@ 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>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
@edit-environment="editEnvironment('Global')"
|
||||
/>
|
||||
</div>
|
||||
<EnvironmentsMy v-if="environmentType.type === 'my-environments'" />
|
||||
<EnvironmentsMy v-show="environmentType.type === 'my-environments'" />
|
||||
<EnvironmentsTeams
|
||||
v-if="environmentType.type === 'team-environments'"
|
||||
v-show="environmentType.type === 'team-environments'"
|
||||
:team="environmentType.selectedTeam"
|
||||
:team-environments="teamEnvironmentList"
|
||||
:loading="loading"
|
||||
@@ -198,10 +198,15 @@ const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
}
|
||||
|
||||
defineActionHandler("modals.environment.new", () => {
|
||||
action.value = "new"
|
||||
showModalDetails.value = true
|
||||
})
|
||||
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
editingVariableName.value = variableName
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
envName === "Global" && editEnvironment("Global")
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,22 +7,15 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<div class="relative flex">
|
||||
<input
|
||||
id="selectLabelEnvEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="editingEnvironmentIndex === 'Global'"
|
||||
@keyup.enter="saveEnvironment"
|
||||
/>
|
||||
<label for="selectLabelEnvEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
:disabled="editingEnvironmentIndex === 'Global'"
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between flex-1">
|
||||
<label for="variableList" class="p-4">
|
||||
{{ t("environment.variable_list") }}
|
||||
@@ -88,7 +81,6 @@
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -178,7 +170,7 @@ const emit = defineEmits<{
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
const name = ref<string | null>(null)
|
||||
const editingName = ref<string | null>(null)
|
||||
const vars = ref<EnvironmentVariable[]>([
|
||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
||||
])
|
||||
@@ -231,10 +223,12 @@ const liveEnvs = computed(() => {
|
||||
}
|
||||
|
||||
if (props.editingEnvironmentIndex === "Global") {
|
||||
return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))]
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: name.value! })),
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||
]
|
||||
}
|
||||
@@ -244,7 +238,7 @@ watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
name.value = workingEnv.value?.name ?? null
|
||||
editingName.value = workingEnv.value?.name ?? null
|
||||
vars.value = pipe(
|
||||
workingEnv.value?.variables ?? [],
|
||||
A.map((e) => ({
|
||||
@@ -277,7 +271,7 @@ const removeEnvironmentVariable = (index: number) => {
|
||||
}
|
||||
|
||||
const saveEnvironment = () => {
|
||||
if (!name.value) {
|
||||
if (!editingName.value) {
|
||||
toast.error(`${t("environment.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
@@ -293,13 +287,13 @@ const saveEnvironment = () => {
|
||||
)
|
||||
|
||||
const environmentUpdated: Environment = {
|
||||
name: name.value,
|
||||
name: editingName.value,
|
||||
variables: filterdVariables,
|
||||
}
|
||||
|
||||
if (props.action === "new") {
|
||||
// Creating a new environment
|
||||
createEnvironment(name.value, environmentUpdated.variables)
|
||||
createEnvironment(editingName.value, environmentUpdated.variables)
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "MY_ENV",
|
||||
index: envList.value.length - 1,
|
||||
@@ -337,7 +331,7 @@ const saveEnvironment = () => {
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = null
|
||||
editingName.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -158,5 +158,7 @@ const duplicateEnvironments = () => {
|
||||
cloneDeep(getGlobalVariables())
|
||||
)
|
||||
} else duplicateEnvironment(props.environmentIndex)
|
||||
|
||||
toast.success(`${t("environment.duplicated")}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
class="mb-4"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -109,7 +108,7 @@ const resetSelectedData = () => {
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
editingVariableName.value = variableName
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const envIndex: number = environments.value.findIndex(
|
||||
(environment: Environment) => {
|
||||
return environment.name === envName
|
||||
|
||||
@@ -7,23 +7,15 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col px-2">
|
||||
<div class="relative flex">
|
||||
<input
|
||||
id="selectLabelEnvEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
:class="isViewer && 'opacity-25'"
|
||||
placeholder=""
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="isViewer"
|
||||
@keyup.enter="saveEnvironment"
|
||||
/>
|
||||
<label for="selectLabelEnvEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:input-styles="['floating-input', isViewer && 'opacity-25']"
|
||||
:label="t('action.label')"
|
||||
:disabled="isViewer"
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between flex-1">
|
||||
<label for="variableList" class="p-4">
|
||||
{{ t("environment.variable_list") }}
|
||||
@@ -94,13 +86,11 @@
|
||||
disabled
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -190,7 +180,7 @@ const emit = defineEmits<{
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
const name = ref<string | null>(null)
|
||||
const editingName = ref<string | null>(null)
|
||||
const vars = ref<EnvironmentVariable[]>([
|
||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
||||
])
|
||||
@@ -216,7 +206,9 @@ const liveEnvs = computed(() => {
|
||||
if (evnExpandError.value) {
|
||||
return []
|
||||
} else {
|
||||
return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))]
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -225,7 +217,7 @@ watch(
|
||||
(show) => {
|
||||
if (show) {
|
||||
if (props.action === "new") {
|
||||
name.value = null
|
||||
editingName.value = null
|
||||
vars.value = pipe(
|
||||
props.envVars() ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
@@ -234,7 +226,7 @@ watch(
|
||||
}))
|
||||
)
|
||||
} else if (props.editingEnvironment !== null) {
|
||||
name.value = props.editingEnvironment.environment.name ?? null
|
||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
||||
vars.value = pipe(
|
||||
props.editingEnvironment.environment.variables ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
@@ -272,7 +264,7 @@ const isLoading = ref(false)
|
||||
const saveEnvironment = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
if (!name.value) {
|
||||
if (!editingName.value) {
|
||||
toast.error(`${t("environment.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
@@ -297,7 +289,7 @@ const saveEnvironment = async () => {
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
props.editingTeamId,
|
||||
name.value
|
||||
editingName.value
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
@@ -320,7 +312,7 @@ const saveEnvironment = async () => {
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
props.editingEnvironment.id,
|
||||
name.value
|
||||
editingName.value
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
@@ -339,7 +331,7 @@ const saveEnvironment = async () => {
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = null
|
||||
editingName.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ const duplicateEnvironments = () => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("team_environment.duplicate")}`)
|
||||
toast.success(`${t("environment.duplicated")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
filled
|
||||
class="mb-4"
|
||||
:icon="IconPlus"
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
@@ -64,7 +63,6 @@
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
class="mb-4"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -178,7 +176,7 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
defineActionHandler(
|
||||
"modals.team.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
editingVariableName.value = variableName
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const teamEnvToEdit = props.teamEnvironments.find(
|
||||
(environment) => environment.environment.name === envName
|
||||
)
|
||||
|
||||
@@ -9,27 +9,22 @@
|
||||
<template #body>
|
||||
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
|
||||
<HoppSmartItem
|
||||
:loading="signingInWithGitHub"
|
||||
:icon="IconGithub"
|
||||
:label="`${t('auth.continue_with_github')}`"
|
||||
@click="signInWithGithub"
|
||||
v-for="provider in allowedAuthProviders"
|
||||
:key="provider.id"
|
||||
:loading="provider.isLoading.value"
|
||||
:icon="provider.icon"
|
||||
:label="provider.label"
|
||||
@click="provider.action"
|
||||
/>
|
||||
|
||||
<hr v-if="additonalLoginItems.length > 0" />
|
||||
|
||||
<HoppSmartItem
|
||||
: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'"
|
||||
v-for="loginItem in additonalLoginItems"
|
||||
:key="loginItem.id"
|
||||
:icon="loginItem.icon"
|
||||
:label="loginItem.text(t)"
|
||||
@click="doAdditionalLoginItemClickAction(loginItem)"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
@@ -37,24 +32,14 @@
|
||||
class="flex flex-col space-y-2"
|
||||
@submit.prevent="signInWithEmail"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="off"
|
||||
required
|
||||
spellcheck="false"
|
||||
autofocus
|
||||
/>
|
||||
<label for="email">
|
||||
{{ t("auth.email") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder=" "
|
||||
:label="t('auth.email')"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
|
||||
<HoppButtonPrimary
|
||||
:loading="signingInWithEmail"
|
||||
type="submit"
|
||||
@@ -123,124 +108,138 @@
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
<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"
|
||||
|
||||
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"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
emits: ["hide-modal"],
|
||||
setup() {
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
import { LoginItemDef } from "~/platform/auth"
|
||||
|
||||
const tosLink = import.meta.env.VITE_APP_TOS_LINK
|
||||
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
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 emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
this.subscribeToStream(currentUser$, (user) => {
|
||||
if (user) this.hideModal()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
showLoginSuccess() {
|
||||
this.toast.success(`${this.t("auth.login_success")}`)
|
||||
},
|
||||
async signInWithGoogle() {
|
||||
this.signingInWithGoogle = true
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
try {
|
||||
await platform.auth.signInUserWithGoogle()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/*
|
||||
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)
|
||||
/*
|
||||
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
|
||||
*/
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
}
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
|
||||
this.signingInWithGoogle = false
|
||||
},
|
||||
async signInWithGithub() {
|
||||
this.signingInWithGitHub = true
|
||||
signingInWithGoogle.value = false
|
||||
}
|
||||
|
||||
const result = await platform.auth.signInUserWithGithub()
|
||||
const signInWithGithub = async () => {
|
||||
signingInWithGitHub.value = true
|
||||
|
||||
if (!result) {
|
||||
this.signingInWithGitHub = false
|
||||
return
|
||||
}
|
||||
const result = await platform.auth.signInUserWithGithub()
|
||||
|
||||
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) {
|
||||
signingInWithGitHub.value = false
|
||||
return
|
||||
}
|
||||
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.log("error logging into github", result.err)
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
}
|
||||
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()
|
||||
|
||||
this.signingInWithGitHub = false
|
||||
},
|
||||
async signInWithMicrosoft() {
|
||||
this.signingInWithMicrosoft = true
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.log("error logging into github", result.err)
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
|
||||
try {
|
||||
await platform.auth.signInUserWithMicrosoft()
|
||||
// this.showLoginSuccess()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
/*
|
||||
signingInWithGitHub.value = false
|
||||
}
|
||||
|
||||
const signInWithMicrosoft = async () => {
|
||||
signingInWithMicrosoft.value = true
|
||||
|
||||
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:
|
||||
@@ -248,34 +247,82 @@ export default defineComponent({
|
||||
@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
|
||||
*/
|
||||
this.toast.error(`${this.t("error.something_went_wrong")}`)
|
||||
}
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
|
||||
this.signingInWithMicrosoft = false
|
||||
},
|
||||
async signInWithEmail() {
|
||||
this.signingInWithEmail = true
|
||||
signingInWithMicrosoft.value = false
|
||||
}
|
||||
|
||||
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")
|
||||
},
|
||||
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"
|
||||
},
|
||||
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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
|
||||
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
@@ -32,7 +32,7 @@
|
||||
:active="authName === 'None'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'none'
|
||||
auth.authType = 'none'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -43,7 +43,7 @@
|
||||
:active="authName === 'Basic Auth'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'basic'
|
||||
auth.authType = 'basic'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -54,7 +54,7 @@
|
||||
:active="authName === 'Bearer'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'bearer'
|
||||
auth.authType = 'bearer'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -65,7 +65,7 @@
|
||||
:active="authName === 'OAuth 2.0'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'oauth-2'
|
||||
auth.authType = 'oauth-2'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -76,7 +76,7 @@
|
||||
:active="authName === 'API key'"
|
||||
@click="
|
||||
() => {
|
||||
authType = 'api-key'
|
||||
auth.authType = 'api-key'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -90,8 +90,8 @@
|
||||
:on="!URLExcludes.auth"
|
||||
@change="setExclude('auth', !$event)"
|
||||
>
|
||||
{{ t("authorization.include_in_url") }}
|
||||
</HoppSmartCheckbox> -->
|
||||
{{ $t("authorization.include_in_url") }}
|
||||
</HoppSmartCheckbox>-->
|
||||
<HoppSmartCheckbox
|
||||
:on="authActive"
|
||||
class="px-2"
|
||||
@@ -115,7 +115,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="authType === 'none'"
|
||||
v-if="auth.authType === 'none'"
|
||||
:src="`/images/states/${colorMode.value}/login.svg`"
|
||||
:alt="`${t('empty.authorization')}`"
|
||||
:text="t('empty.authorization')"
|
||||
@@ -127,114 +127,47 @@
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div v-if="authType === 'basic'">
|
||||
<div v-if="auth.authType === 'basic'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicUsername"
|
||||
v-model="auth.username"
|
||||
:environment-highlights="false"
|
||||
:placeholder="t('authorization.username')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicPassword"
|
||||
v-model="auth.password"
|
||||
:environment-highlights="false"
|
||||
:placeholder="t('authorization.password')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'bearer'">
|
||||
<div v-if="auth.authType === 'bearer'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="bearerToken"
|
||||
v-model="auth.token"
|
||||
:environment-highlights="false"
|
||||
placeholder="Token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'oauth-2'">
|
||||
<div v-if="auth.authType === 'oauth-2'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="oauth2Token"
|
||||
v-model="auth.token"
|
||||
:environment-highlights="false"
|
||||
placeholder="Token"
|
||||
/>
|
||||
</div>
|
||||
<HttpOAuth2Authorization />
|
||||
<HttpOAuth2Authorization v-model="auth" />
|
||||
</div>
|
||||
<div v-if="authType === 'api-key'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="apiKey"
|
||||
:environment-highlights="false"
|
||||
placeholder="Key"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="apiValue"
|
||||
:environment-highlights="false"
|
||||
placeholder="Value"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center border-b border-dividerLight">
|
||||
<span class="flex items-center">
|
||||
<label class="ml-4 text-secondaryLight">
|
||||
{{ t("authorization.pass_key_by") }}
|
||||
</label>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => authTippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppButtonSecondary
|
||||
:label="addTo || t('state.none')"
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="authTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
|
||||
:active="addTo === 'Headers'"
|
||||
:label="'Headers'"
|
||||
@click="
|
||||
() => {
|
||||
addTo = 'Headers'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="
|
||||
addTo === 'Query params' ? IconCircleDot : IconCircle
|
||||
"
|
||||
:active="addTo === 'Query params'"
|
||||
:label="'Query params'"
|
||||
@click="
|
||||
() => {
|
||||
addTo = 'Query params'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="auth.authType === 'api-key'">
|
||||
<HttpAuthorizationApiKey v-model="auth" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -257,55 +190,45 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, Ref } from "vue"
|
||||
import {
|
||||
HoppGQLAuthAPIKey,
|
||||
HoppGQLAuthBasic,
|
||||
HoppGQLAuthBearer,
|
||||
HoppGQLAuthOAuth2,
|
||||
} from "@hoppscotch/data"
|
||||
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { gqlAuth$, setGQLAuth } from "~/newstore/GQLSession"
|
||||
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import { computed, ref } from "vue"
|
||||
import { HoppGQLAuth } from "@hoppscotch/data"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const auth = useStream(
|
||||
gqlAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setGQLAuth
|
||||
)
|
||||
const props = defineProps<{
|
||||
modelValue: HoppGQLAuth
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppGQLAuth): void
|
||||
}>()
|
||||
|
||||
const auth = useVModel(props, "modelValue", emit)
|
||||
|
||||
const AUTH_KEY_NAME = {
|
||||
basic: "Basic Auth",
|
||||
bearer: "Bearer",
|
||||
"oauth-2": "OAuth 2.0",
|
||||
"api-key": "API key",
|
||||
none: "None",
|
||||
} as const
|
||||
|
||||
const authType = pluckRef(auth, "authType")
|
||||
const authName = computed(() => {
|
||||
if (authType.value === "basic") return "Basic Auth"
|
||||
else if (authType.value === "bearer") return "Bearer"
|
||||
else if (authType.value === "oauth-2") return "OAuth 2.0"
|
||||
else if (authType.value === "api-key") return "API key"
|
||||
else return "None"
|
||||
})
|
||||
const authName = computed(() =>
|
||||
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
|
||||
)
|
||||
const authActive = pluckRef(auth, "authActive")
|
||||
const basicUsername = pluckRef(auth as Ref<HoppGQLAuthBasic>, "username")
|
||||
const basicPassword = pluckRef(auth as Ref<HoppGQLAuthBasic>, "password")
|
||||
const bearerToken = pluckRef(auth as Ref<HoppGQLAuthBearer>, "token")
|
||||
const oauth2Token = pluckRef(auth as Ref<HoppGQLAuthOAuth2>, "token")
|
||||
const apiKey = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "key")
|
||||
const apiValue = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "value")
|
||||
const addTo = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "addTo")
|
||||
if (typeof addTo.value === "undefined") {
|
||||
addTo.value = "Headers"
|
||||
apiKey.value = ""
|
||||
apiValue.value = ""
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
auth.value = {
|
||||
@@ -316,5 +239,4 @@ const clearContent = () => {
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const authTippyActions = ref<any | null>(null)
|
||||
</script>
|
||||
|
||||
430
packages/hoppscotch-common/src/components/graphql/Headers.vue
Normal file
430
packages/hoppscotch-common/src/components/graphql/Headers.vue
Normal file
@@ -0,0 +1,430 @@
|
||||
<template>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("tab.headers") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.bulk_mode')"
|
||||
:icon="IconEdit"
|
||||
:class="{ '!text-accent': bulkMode }"
|
||||
@click="bulkMode = !bulkMode"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
:disabled="bulkMode"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
|
||||
<div v-else>
|
||||
<draggable
|
||||
v-model="workingHeaders"
|
||||
:item-key="(header: any) => `header-${header.id}`"
|
||||
animation="250"
|
||||
handle=".draggable-handle"
|
||||
draggable=".draggable-content"
|
||||
ghost-class="cursor-move"
|
||||
chosen-class="bg-primaryLight"
|
||||
drag-class="cursor-grabbing"
|
||||
>
|
||||
<template #item="{ element: header, index }">
|
||||
<div
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
content:
|
||||
index !== workingHeaders?.length - 1
|
||||
? t('action.drag_to_reorder')
|
||||
: null,
|
||||
}"
|
||||
:icon="IconGripVertical"
|
||||
class="cursor-auto text-primary hover:text-primary"
|
||||
:class="{
|
||||
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
|
||||
index !== workingHeaders?.length - 1,
|
||||
}"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<HoppSmartAutoComplete
|
||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||
: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,
|
||||
value: header.value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:name="`value ${String(index)}`"
|
||||
:value="header.value"
|
||||
autofocus
|
||||
@change="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
key: header.key,
|
||||
value: ($event!.target! as HTMLInputElement).value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
header.hasOwnProperty('active')
|
||||
? header.active
|
||||
? t('action.turn_off')
|
||||
: t('action.turn_on')
|
||||
: t('action.turn_off')
|
||||
"
|
||||
:icon="
|
||||
header.hasOwnProperty('active')
|
||||
? header.active
|
||||
? IconCheckCircle
|
||||
: IconCircle
|
||||
: IconCheckCircle
|
||||
"
|
||||
color="green"
|
||||
@click="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
key: header.key,
|
||||
value: header.value,
|
||||
active: !header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@click="deleteHeader(index)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingHeaders.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
||||
:alt="`${t('empty.headers')}`"
|
||||
:text="t('empty.headers')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
:icon="IconPlus"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconGripVertical from "~icons/lucide/grip-vertical"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import {
|
||||
GQLHeader,
|
||||
rawKeyValueEntriesToString,
|
||||
parseRawKeyValueEntriesE,
|
||||
RawKeyValueEntry,
|
||||
HoppGQLRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import draggable from "vuedraggable-es"
|
||||
import { clone, cloneDeep, isEqual } from "lodash-es"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { commonHeaders } from "~/helpers/headers"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { objRemoveKey } from "~/helpers/functional/object"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// v-model integration with props and emit
|
||||
const props = defineProps<{ modelValue: HoppGQLRequest }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppGQLRequest): void
|
||||
}>()
|
||||
|
||||
const request = useVModel(props, "modelValue", emit)
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
const linewrapEnabled = ref(false)
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
|
||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||
|
||||
const bulkEditor = ref<any | null>(null)
|
||||
|
||||
useCodemirror(
|
||||
bulkEditor,
|
||||
bulkHeaders,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
// The UI representation of the headers list (has the empty end header)
|
||||
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
|
||||
{
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
},
|
||||
])
|
||||
|
||||
// Rule: Working Headers always have one empty header or the last element is always an empty header
|
||||
watch(workingHeaders, (headersList) => {
|
||||
if (
|
||||
headersList.length > 0 &&
|
||||
headersList[headersList.length - 1].key !== ""
|
||||
) {
|
||||
workingHeaders.value.push({
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Sync logic between headers and working headers
|
||||
watch(
|
||||
props.modelValue.headers,
|
||||
(newHeadersList) => {
|
||||
// Sync should overwrite working headers
|
||||
const filteredWorkingHeaders = pipe(
|
||||
workingHeaders.value,
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((e) => e.key !== ""),
|
||||
O.map(objRemoveKey("id"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const filteredBulkHeaders = pipe(
|
||||
parseRawKeyValueEntriesE(bulkHeaders.value),
|
||||
E.map(
|
||||
flow(
|
||||
RA.filter((e) => e.key !== ""),
|
||||
RA.toArray
|
||||
)
|
||||
),
|
||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||
)
|
||||
|
||||
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
|
||||
workingHeaders.value = pipe(
|
||||
newHeadersList,
|
||||
A.map((x) => ({ id: idTicker.value++, ...x }))
|
||||
)
|
||||
}
|
||||
|
||||
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
|
||||
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(workingHeaders, (newWorkingHeaders) => {
|
||||
const fixedHeaders = pipe(
|
||||
newWorkingHeaders,
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((e) => e.key !== ""),
|
||||
O.map(objRemoveKey("id"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (!isEqual(request.value.headers, fixedHeaders)) {
|
||||
request.value.headers = cloneDeep(fixedHeaders)
|
||||
}
|
||||
})
|
||||
|
||||
// Bulk Editor Syncing with Working Headers
|
||||
watch(bulkHeaders, (newBulkHeaders) => {
|
||||
const filteredBulkHeaders = pipe(
|
||||
parseRawKeyValueEntriesE(newBulkHeaders),
|
||||
E.map(
|
||||
flow(
|
||||
RA.filter((e) => e.key !== ""),
|
||||
RA.toArray
|
||||
)
|
||||
),
|
||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||
)
|
||||
|
||||
if (!isEqual(request.value.headers, filteredBulkHeaders)) {
|
||||
request.value.headers = filteredBulkHeaders
|
||||
}
|
||||
})
|
||||
|
||||
watch(workingHeaders, (newHeadersList) => {
|
||||
// If we are in bulk mode, don't apply direct changes
|
||||
if (bulkMode.value) return
|
||||
|
||||
try {
|
||||
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
|
||||
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
|
||||
value: item.substring(item.indexOf(":") + 1).trimLeft(),
|
||||
active: !item.trim().startsWith("#"),
|
||||
}))
|
||||
|
||||
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
|
||||
|
||||
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
|
||||
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
const addHeader = () => {
|
||||
workingHeaders.value.push({
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
|
||||
workingHeaders.value = workingHeaders.value.map((h, i) =>
|
||||
i === index ? header : h
|
||||
)
|
||||
}
|
||||
|
||||
const deleteHeader = (index: number) => {
|
||||
const headersBeforeDeletion = clone(workingHeaders.value)
|
||||
|
||||
if (
|
||||
!(
|
||||
headersBeforeDeletion.length > 0 &&
|
||||
index === headersBeforeDeletion.length - 1
|
||||
)
|
||||
) {
|
||||
if (deletionToast.value) {
|
||||
deletionToast.value.goAway(0)
|
||||
deletionToast.value = null
|
||||
}
|
||||
|
||||
deletionToast.value = toast.success(`${t("state.deleted")}`, {
|
||||
action: [
|
||||
{
|
||||
text: `${t("action.undo")}`,
|
||||
onClick: (_: any, toastObject: any) => {
|
||||
workingHeaders.value = headersBeforeDeletion
|
||||
toastObject.goAway(0)
|
||||
deletionToast.value = null
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
onComplete: () => {
|
||||
deletionToast.value = null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
workingHeaders.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
// set headers list to the initial state
|
||||
workingHeaders.value = [
|
||||
{
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
},
|
||||
]
|
||||
|
||||
bulkHeaders.value = ""
|
||||
}
|
||||
</script>
|
||||
237
packages/hoppscotch-common/src/components/graphql/Query.vue
Normal file
237
packages/hoppscotch-common/src/components/graphql/Query.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight gqlRunQuery"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("request.query") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-if="subscriptionState === 'SUBSCRIBED'"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
allowHTML: true,
|
||||
}"
|
||||
:title="`${t('request.stop')}`"
|
||||
:label="`${t('request.stop')}`"
|
||||
:icon="IconStop"
|
||||
class="rounded-none !text-accent !hover:text-accentDark"
|
||||
@click="unsubscribe()"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
v-if="selectedOperation && subscriptionState !== 'SUBSCRIBED'"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
allowHTML: true,
|
||||
}"
|
||||
:title="`${t('request.run')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
|
||||
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
|
||||
:icon="IconPlay"
|
||||
:disabled="!selectedOperation"
|
||||
class="rounded-none !text-accent !hover:text-accentDark"
|
||||
@click="runQuery(selectedOperation)"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t(
|
||||
'request.save'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||
:label="`${t('request.save')}`"
|
||||
:icon="IconSave"
|
||||
class="rounded-none"
|
||||
@click="saveRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearGQLQuery()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.prettify')"
|
||||
:icon="prettifyQueryIcon"
|
||||
@click="prettifyQuery"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyQueryIcon"
|
||||
@click="copyQuery"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="queryEditor" class="flex flex-col flex-1"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
import IconStop from "~icons/lucide/stop-circle"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconWand from "~icons/lucide/wand"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { onMounted, reactive, ref, markRaw } from "vue"
|
||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import * as gql from "graphql"
|
||||
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
|
||||
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
|
||||
import { selectedGQLOpHighlight } from "~/helpers/editor/gql/operation"
|
||||
import { debounce } from "lodash-es"
|
||||
import { ViewUpdate } from "@codemirror/view"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import {
|
||||
schema,
|
||||
socketDisconnect,
|
||||
subscriptionState,
|
||||
} from "~/helpers/graphql/connection"
|
||||
|
||||
// Template refs
|
||||
const queryEditor = ref<any | null>(null)
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save-request"): void
|
||||
(e: "update:modelValue", val: string): void
|
||||
(e: "run-query", definition: gql.OperationDefinitionNode | null): void
|
||||
}>()
|
||||
|
||||
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
const prettifyQueryIcon = refAutoReset<
|
||||
typeof IconWand | typeof IconCheck | typeof IconInfo
|
||||
>(IconWand, 1000)
|
||||
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
||||
|
||||
const gqlQueryString = useVModel(props, "modelValue", emit)
|
||||
|
||||
const debouncedOnUpdateQueryState = debounce((update: ViewUpdate) => {
|
||||
if (!update.selectionSet) return
|
||||
|
||||
const selectedPos = update.state.selection.main.head
|
||||
const queryString = update.state.doc.toJSON().join(update.state.lineBreak)
|
||||
|
||||
try {
|
||||
const operations = gql.parse(queryString)
|
||||
if (operations.definitions.length === 1) {
|
||||
selectedOperation.value = operations
|
||||
.definitions[0] as gql.OperationDefinitionNode
|
||||
return
|
||||
}
|
||||
|
||||
selectedOperation.value =
|
||||
(operations.definitions.find((def) => {
|
||||
if (def.kind !== "OperationDefinition") return false
|
||||
const { start, end } = def.loc!
|
||||
return selectedPos >= start && selectedPos <= end
|
||||
}) as gql.OperationDefinitionNode) ?? null
|
||||
} catch (error) {
|
||||
// console.error(error)
|
||||
}
|
||||
}, 300)
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
const operations = gql.parse(gqlQueryString.value)
|
||||
if (operations.definitions.length) {
|
||||
selectedOperation.value = operations
|
||||
.definitions[0] as gql.OperationDefinitionNode
|
||||
return
|
||||
}
|
||||
} catch (error) {}
|
||||
})
|
||||
|
||||
useCodemirror(
|
||||
queryEditor,
|
||||
gqlQueryString,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "graphql",
|
||||
placeholder: `${t("request.query")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: createGQLQueryLinter(schema),
|
||||
completer: queryCompleter(schema),
|
||||
environmentHighlights: false,
|
||||
additionalExts: [markRaw(selectedGQLOpHighlight)],
|
||||
onUpdate: debouncedOnUpdateQueryState,
|
||||
})
|
||||
)
|
||||
|
||||
// operations on graphql query string
|
||||
// const operations = useReadonlyStream(props.request.operations$, [])
|
||||
|
||||
const prettifyQuery = () => {
|
||||
try {
|
||||
gqlQueryString.value = gql.print(
|
||||
gql.parse(gqlQueryString.value, {
|
||||
allowLegacyFragmentVariables: true,
|
||||
})
|
||||
)
|
||||
prettifyQueryIcon.value = IconCheck
|
||||
} catch (e) {
|
||||
toast.error(`${t("error.gql_prettify_invalid_query")}`)
|
||||
prettifyQueryIcon.value = IconInfo
|
||||
}
|
||||
}
|
||||
|
||||
const copyQuery = () => {
|
||||
copyToClipboard(gqlQueryString.value)
|
||||
copyQueryIcon.value = IconCheck
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const clearGQLQuery = () => {
|
||||
gqlQueryString.value = ""
|
||||
}
|
||||
|
||||
const runQuery = (definition: gql.OperationDefinitionNode | null = null) => {
|
||||
emit("run-query", definition)
|
||||
}
|
||||
const unsubscribe = () => {
|
||||
socketDisconnect()
|
||||
}
|
||||
const saveRequest = () => {
|
||||
emit("save-request")
|
||||
}
|
||||
|
||||
defineActionHandler("editor.format", prettifyQuery)
|
||||
</script>
|
||||
@@ -17,55 +17,127 @@
|
||||
<HoppButtonPrimary
|
||||
id="get"
|
||||
name="get"
|
||||
:loading="isLoading"
|
||||
:loading="connection.state === 'CONNECTING'"
|
||||
:label="!connected ? t('action.connect') : t('action.disconnect')"
|
||||
class="w-32"
|
||||
@click="onConnectClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartModal
|
||||
v-if="connectionSwitchModal"
|
||||
dialog
|
||||
:dimissible="false"
|
||||
:title="t('graphql.switch_connection')"
|
||||
@close="connectionSwitchModal = false"
|
||||
>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
{{ t("graphql.connection_switch_url") }}:
|
||||
<kbd class="shortcut-key !ml-0"> {{ lastTwoUrls.at(0) }} </kbd>
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
{{ t("graphql.connection_switch_new_url") }}:
|
||||
<kbd class="shortcut-key !ml-0"> {{ lastTwoUrls.at(1) }} </kbd>
|
||||
</p>
|
||||
<p>{{ t("graphql.connection_switch_confirm") }}</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('action.connect')"
|
||||
:loading="connection.state === 'CONNECTING'"
|
||||
outline
|
||||
@click="switchConnection()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="cancelSwitch()"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
gqlAuth$,
|
||||
gqlHeaders$,
|
||||
gqlURL$,
|
||||
setGQLURL,
|
||||
} from "~/newstore/GQLSession"
|
||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { connection } from "~/helpers/graphql/connection"
|
||||
import { connect } from "~/helpers/graphql/connection"
|
||||
import { disconnect } from "~/helpers/graphql/connection"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
conn: GQLConnection
|
||||
}>()
|
||||
const interceptorService = useService(InterceptorService)
|
||||
|
||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
|
||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||
const auth = useReadonlyStream(gqlAuth$, {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
const connectionSwitchModal = ref(false)
|
||||
|
||||
const connected = computed(() => connection.state === "CONNECTED")
|
||||
|
||||
const url = computed({
|
||||
get: () => currentActiveTab.value?.document.request.url ?? "",
|
||||
set: (value) => {
|
||||
currentActiveTab.value!.document.request.url = value
|
||||
},
|
||||
})
|
||||
|
||||
const url = useStream(gqlURL$, "", setGQLURL)
|
||||
|
||||
const onConnectClick = () => {
|
||||
if (!connected.value) {
|
||||
props.conn.connect(url.value, headers.value as any, auth.value)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-schema",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
gqlConnect()
|
||||
} else {
|
||||
props.conn.disconnect()
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
const gqlConnect = () => {
|
||||
connect(url.value, currentActiveTab.value?.document.request.headers)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-schema",
|
||||
strategy: interceptorService.currentInterceptorID.value!,
|
||||
})
|
||||
}
|
||||
|
||||
const switchConnection = () => {
|
||||
gqlConnect()
|
||||
connectionSwitchModal.value = false
|
||||
}
|
||||
|
||||
const lastTwoUrls = ref<string[]>([])
|
||||
|
||||
watch(
|
||||
currentActiveTab,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
lastTwoUrls.value.push(newVal.document.request.url)
|
||||
if (lastTwoUrls.value.length > 2) {
|
||||
lastTwoUrls.value.shift()
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
connected.value &&
|
||||
lastTwoUrls.value.length === 2 &&
|
||||
lastTwoUrls.value.at(0) !== lastTwoUrls.value.at(1)
|
||||
) {
|
||||
disconnect()
|
||||
connectionSwitchModal.value = true
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
const cancelSwitch = () => {
|
||||
if (connected.value) disconnect()
|
||||
connectionSwitchModal.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,311 +2,42 @@
|
||||
<div class="flex flex-col flex-1 h-full">
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
styles="sticky bg-primary z-10"
|
||||
:render-inactive-tabs="true"
|
||||
>
|
||||
<HoppSmartTab
|
||||
:id="'query'"
|
||||
:label="`${t('tab.query')}`"
|
||||
:indicator="gqlQueryString && gqlQueryString.length > 0 ? true : false"
|
||||
:indicator="request.query && request.query.length > 0 ? true : false"
|
||||
>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold gqlRunQuery"
|
||||
>
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ t("request.query") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t(
|
||||
'request.run'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||
:label="`${t('request.run')}`"
|
||||
:icon="IconPlay"
|
||||
class="rounded-none !text-accent !hover:text-accentDark"
|
||||
@click="runQuery()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t(
|
||||
'request.save'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||
:label="`${t('request.save')}`"
|
||||
:icon="IconSave"
|
||||
class="rounded-none"
|
||||
@click="saveRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearGQLQuery()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabledQuery }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabledQuery = !linewrapEnabledQuery"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.prettify')"
|
||||
:icon="prettifyQueryIcon"
|
||||
@click="prettifyQuery"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyQueryIcon"
|
||||
@click="copyQuery"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="queryEditor" class="flex flex-col flex-1"></div>
|
||||
<GraphqlQuery
|
||||
v-model="request.query"
|
||||
@run-query="runQuery"
|
||||
@save-request="saveRequest"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'variables'"
|
||||
:label="`${t('tab.variables')}`"
|
||||
:indicator="variableString && variableString.length > 0 ? true : false"
|
||||
:indicator="
|
||||
request.variables && request.variables.length > 0 ? true : false
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
|
||||
>
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ t("request.variables") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearGQLVariables()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabledVariable }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="
|
||||
linewrapEnabledVariable = !linewrapEnabledVariable
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.prettify')"
|
||||
:icon="prettifyVariablesIcon"
|
||||
@click="prettifyVariableString"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyVariablesIcon"
|
||||
@click="copyVariables"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="variableEditor" class="flex flex-col flex-1"></div>
|
||||
<GraphqlVariable
|
||||
v-model="request.variables"
|
||||
@run-query="runQuery"
|
||||
@save-request="saveRequest"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'headers'"
|
||||
:label="`${t('tab.headers')}`"
|
||||
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
|
||||
>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
|
||||
>
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ t("tab.headers") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.bulk_mode')"
|
||||
:icon="IconEdit"
|
||||
:class="{ '!text-accent': bulkMode }"
|
||||
@click="bulkMode = !bulkMode"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
:disabled="bulkMode"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="bulkMode"
|
||||
ref="bulkEditor"
|
||||
class="flex flex-col flex-1"
|
||||
></div>
|
||||
<div v-else>
|
||||
<draggable
|
||||
v-model="workingHeaders"
|
||||
:item-key="(header) => `header-${header.id}`"
|
||||
animation="250"
|
||||
handle=".draggable-handle"
|
||||
draggable=".draggable-content"
|
||||
ghost-class="cursor-move"
|
||||
chosen-class="bg-primaryLight"
|
||||
drag-class="cursor-grabbing"
|
||||
>
|
||||
<template #item="{ element: header, index }">
|
||||
<div
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
content:
|
||||
index !== workingHeaders?.length - 1
|
||||
? t('action.drag_to_reorder')
|
||||
: null,
|
||||
}"
|
||||
:icon="IconGripVertical"
|
||||
class="cursor-auto text-primary hover:text-primary"
|
||||
:class="{
|
||||
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
|
||||
index !== workingHeaders?.length - 1,
|
||||
}"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<HoppSmartAutoComplete
|
||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||
: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,
|
||||
value: header.value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:name="`value ${String(index)}`"
|
||||
:value="header.value"
|
||||
autofocus
|
||||
@change="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
key: header.key,
|
||||
value: ($event!.target! as HTMLInputElement).value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
header.hasOwnProperty('active')
|
||||
? header.active
|
||||
? t('action.turn_off')
|
||||
: t('action.turn_on')
|
||||
: t('action.turn_off')
|
||||
"
|
||||
:icon="
|
||||
header.hasOwnProperty('active')
|
||||
? header.active
|
||||
? IconCheckCircle
|
||||
: IconCircle
|
||||
: IconCheckCircle
|
||||
"
|
||||
color="green"
|
||||
@click="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
key: header.key,
|
||||
value: header.value,
|
||||
active: !header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@click="deleteHeader(index)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingHeaders.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
||||
:alt="`${t('empty.headers')}`"
|
||||
:text="t('empty.headers')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
:icon="IconPlus"
|
||||
class="mb-4"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
<GraphqlHeaders v-model="request" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
<GraphqlAuthorization />
|
||||
<GraphqlAuthorization v-model="request.auth" />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
<CollectionsSaveRequest
|
||||
@@ -318,480 +49,142 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconGripVertical from "~icons/lucide/grip-vertical"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconWand2 from "~icons/lucide/wand-2"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { Ref, computed, reactive, ref, watch } from "vue"
|
||||
import * as gql from "graphql"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as RA from "fp-ts/ReadonlyArray"
|
||||
import { pipe, flow } from "fp-ts/function"
|
||||
import {
|
||||
GQLHeader,
|
||||
makeGQLRequest,
|
||||
rawKeyValueEntriesToString,
|
||||
parseRawKeyValueEntriesE,
|
||||
RawKeyValueEntry,
|
||||
} from "@hoppscotch/data"
|
||||
import draggable from "vuedraggable-es"
|
||||
import { clone, cloneDeep, isEqual } from "lodash-es"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||
import {
|
||||
gqlAuth$,
|
||||
gqlHeaders$,
|
||||
gqlQuery$,
|
||||
gqlResponse$,
|
||||
gqlURL$,
|
||||
gqlVariables$,
|
||||
setGQLAuth,
|
||||
setGQLHeaders,
|
||||
setGQLQuery,
|
||||
setGQLResponse,
|
||||
setGQLVariables,
|
||||
} from "~/newstore/GQLSession"
|
||||
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"
|
||||
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
|
||||
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
|
||||
import * as gql from "graphql"
|
||||
import { clone } from "lodash-es"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { objRemoveKey } from "~/helpers/functional/object"
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { platform } from "~/platform"
|
||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import {
|
||||
GQLResponseEvent,
|
||||
runGQLOperation,
|
||||
gqlMessageEvent,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
|
||||
type OptionTabs = "query" | "headers" | "variables" | "authorization"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const selectedOptionTab = ref<OptionTabs>("query")
|
||||
const interceptorService = useService(InterceptorService)
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
conn: GQLConnection
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const url = useReadonlyStream(gqlURL$, "")
|
||||
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
|
||||
const variableString = useStream(gqlVariables$, "", setGQLVariables)
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
const bulkEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||
|
||||
useCodemirror(
|
||||
bulkEditor,
|
||||
bulkHeaders,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
// The functional headers list (the headers actually in the system)
|
||||
const headers = useStream(gqlHeaders$, [], setGQLHeaders) as Ref<GQLHeader[]>
|
||||
|
||||
const auth = useStream(
|
||||
gqlAuth$,
|
||||
{ authType: "none", authActive: true },
|
||||
setGQLAuth
|
||||
)
|
||||
|
||||
// The UI representation of the headers list (has the empty end header)
|
||||
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
|
||||
// v-model integration with props and emit
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppGQLRequest
|
||||
response?: GQLResponseEvent[] | null
|
||||
tabId: string
|
||||
}>(),
|
||||
{
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
},
|
||||
])
|
||||
|
||||
// Rule: Working Headers always have one empty header or the last element is always an empty header
|
||||
watch(workingHeaders, (headersList) => {
|
||||
if (
|
||||
headersList.length > 0 &&
|
||||
headersList[headersList.length - 1].key !== ""
|
||||
) {
|
||||
workingHeaders.value.push({
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
response: null,
|
||||
}
|
||||
})
|
||||
)
|
||||
const emit = defineEmits(["update:modelValue", "update:response"])
|
||||
|
||||
const request = ref(props.modelValue)
|
||||
|
||||
// Sync logic between headers and working headers
|
||||
watch(
|
||||
headers,
|
||||
(newHeadersList) => {
|
||||
// Sync should overwrite working headers
|
||||
const filteredWorkingHeaders = pipe(
|
||||
workingHeaders.value,
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((e) => e.key !== ""),
|
||||
O.map(objRemoveKey("id"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const filteredBulkHeaders = pipe(
|
||||
parseRawKeyValueEntriesE(bulkHeaders.value),
|
||||
E.map(
|
||||
flow(
|
||||
RA.filter((e) => e.key !== ""),
|
||||
RA.toArray
|
||||
)
|
||||
),
|
||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||
)
|
||||
|
||||
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
|
||||
workingHeaders.value = pipe(
|
||||
newHeadersList,
|
||||
A.map((x) => ({ id: idTicker.value++, ...x }))
|
||||
)
|
||||
}
|
||||
|
||||
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
|
||||
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
|
||||
}
|
||||
() => request.value,
|
||||
(newVal) => {
|
||||
emit("update:modelValue", newVal)
|
||||
},
|
||||
{ immediate: true }
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(workingHeaders, (newWorkingHeaders) => {
|
||||
const fixedHeaders = pipe(
|
||||
newWorkingHeaders,
|
||||
A.filterMap(
|
||||
flow(
|
||||
O.fromPredicate((e) => e.key !== ""),
|
||||
O.map(objRemoveKey("id"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (!isEqual(headers.value, fixedHeaders)) {
|
||||
headers.value = cloneDeep(fixedHeaders)
|
||||
}
|
||||
})
|
||||
|
||||
// Bulk Editor Syncing with Working Headers
|
||||
watch(bulkHeaders, (newBulkHeaders) => {
|
||||
const filteredBulkHeaders = pipe(
|
||||
parseRawKeyValueEntriesE(newBulkHeaders),
|
||||
E.map(
|
||||
flow(
|
||||
RA.filter((e) => e.key !== ""),
|
||||
RA.toArray
|
||||
)
|
||||
),
|
||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||
)
|
||||
|
||||
if (!isEqual(headers.value, filteredBulkHeaders)) {
|
||||
headers.value = filteredBulkHeaders
|
||||
}
|
||||
})
|
||||
|
||||
watch(workingHeaders, (newHeadersList) => {
|
||||
// If we are in bulk mode, don't apply direct changes
|
||||
if (bulkMode.value) return
|
||||
|
||||
try {
|
||||
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
|
||||
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
|
||||
value: item.substring(item.indexOf(":") + 1).trimLeft(),
|
||||
active: !item.trim().startsWith("#"),
|
||||
}))
|
||||
|
||||
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
|
||||
|
||||
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
|
||||
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
const addHeader = () => {
|
||||
workingHeaders.value.push({
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
|
||||
workingHeaders.value = workingHeaders.value.map((h, i) =>
|
||||
i === index ? header : h
|
||||
)
|
||||
}
|
||||
|
||||
const deleteHeader = (index: number) => {
|
||||
const headersBeforeDeletion = clone(workingHeaders.value)
|
||||
|
||||
if (
|
||||
!(
|
||||
headersBeforeDeletion.length > 0 &&
|
||||
index === headersBeforeDeletion.length - 1
|
||||
)
|
||||
) {
|
||||
if (deletionToast.value) {
|
||||
deletionToast.value.goAway(0)
|
||||
deletionToast.value = null
|
||||
}
|
||||
|
||||
deletionToast.value = toast.success(`${t("state.deleted")}`, {
|
||||
action: [
|
||||
{
|
||||
text: `${t("action.undo")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
workingHeaders.value = headersBeforeDeletion
|
||||
toastObject.goAway(0)
|
||||
deletionToast.value = null
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
onComplete: () => {
|
||||
deletionToast.value = null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
workingHeaders.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
// set headers list to the initial state
|
||||
workingHeaders.value = [
|
||||
{
|
||||
id: idTicker.value++,
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
},
|
||||
]
|
||||
|
||||
bulkHeaders.value = ""
|
||||
}
|
||||
const url = computedWithControl(
|
||||
() => currentActiveTab.value,
|
||||
() => currentActiveTab.value.document.request.url
|
||||
)
|
||||
|
||||
const activeGQLHeadersCount = computed(
|
||||
() =>
|
||||
headers.value.filter((x) => x.active && (x.key !== "" || x.value !== ""))
|
||||
.length
|
||||
request.value.headers.filter(
|
||||
(x) => x.active && (x.key !== "" || x.value !== "")
|
||||
).length
|
||||
)
|
||||
|
||||
const variableEditor = ref<any | null>(null)
|
||||
const linewrapEnabledVariable = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
variableEditor,
|
||||
variableString,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
placeholder: `${t("request.variables")}`,
|
||||
lineWrapping: linewrapEnabledVariable,
|
||||
},
|
||||
linter: computed(() =>
|
||||
variableString.value.length > 0 ? jsonLinter : null
|
||||
),
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
const queryEditor = ref<any | null>(null)
|
||||
const schema = useReadonlyStream(props.conn.schema$, null, "noclone")
|
||||
const linewrapEnabledQuery = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
queryEditor,
|
||||
gqlQueryString,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "graphql",
|
||||
placeholder: `${t("request.query")}`,
|
||||
lineWrapping: linewrapEnabledQuery,
|
||||
},
|
||||
linter: createGQLQueryLinter(schema),
|
||||
completer: queryCompleter(schema),
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
const prettifyQueryIcon = refAutoReset<
|
||||
typeof IconWand2 | typeof IconCheck | typeof IconInfo
|
||||
>(IconWand2, 1000)
|
||||
const prettifyVariablesIcon = refAutoReset<
|
||||
typeof IconWand2 | typeof IconCheck | typeof IconInfo
|
||||
>(IconWand2, 1000)
|
||||
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
const copyQuery = () => {
|
||||
copyToClipboard(gqlQueryString.value)
|
||||
copyQueryIcon.value = IconCheck
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const response = useStream(gqlResponse$, "", setGQLResponse)
|
||||
|
||||
const runQuery = async () => {
|
||||
const runQuery = async (
|
||||
definition: gql.OperationDefinitionNode | null = null
|
||||
) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
startPageProgress()
|
||||
response.value = "loading"
|
||||
|
||||
try {
|
||||
const runURL = clone(url.value)
|
||||
const runHeaders = clone(headers.value)
|
||||
const runQuery = clone(gqlQueryString.value)
|
||||
const runVariables = clone(variableString.value)
|
||||
const runAuth = clone(auth.value)
|
||||
const runHeaders = clone(request.value.headers)
|
||||
const runQuery = clone(request.value.query)
|
||||
const runVariables = clone(request.value.variables)
|
||||
const runAuth = clone(request.value.auth)
|
||||
|
||||
const responseText = await props.conn.runQuery(
|
||||
runURL,
|
||||
runHeaders,
|
||||
runQuery,
|
||||
runVariables,
|
||||
runAuth
|
||||
)
|
||||
await runGQLOperation({
|
||||
name: request.value.name,
|
||||
url: runURL,
|
||||
headers: runHeaders,
|
||||
query: runQuery,
|
||||
variables: runVariables,
|
||||
auth: runAuth,
|
||||
operationName: definition?.name?.value,
|
||||
operationType: definition?.operation ?? "query",
|
||||
})
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
completePageProgress()
|
||||
|
||||
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
|
||||
|
||||
addGraphqlHistoryEntry(
|
||||
makeGQLHistoryEntry({
|
||||
request: makeGQLRequest({
|
||||
name: "",
|
||||
url: runURL,
|
||||
query: runQuery,
|
||||
headers: runHeaders,
|
||||
variables: runVariables,
|
||||
auth: runAuth,
|
||||
}),
|
||||
response: response.value,
|
||||
star: false,
|
||||
})
|
||||
)
|
||||
|
||||
toast.success(`${t("state.finished_in", { duration })}`)
|
||||
} catch (e: any) {
|
||||
response.value = `${e}`
|
||||
console.log(e)
|
||||
// response.value = [`${e}`]
|
||||
completePageProgress()
|
||||
|
||||
toast.error(
|
||||
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
|
||||
{}
|
||||
)
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-query",
|
||||
strategy: getCurrentStrategyID(),
|
||||
strategy: interceptorService.currentInterceptorID.value!,
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => gqlMessageEvent.value,
|
||||
(event) => {
|
||||
if (event === "reset") {
|
||||
emit("update:response", [])
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (event?.operationType !== "subscription") {
|
||||
// response.value = [event]
|
||||
emit("update:response", [event])
|
||||
} else {
|
||||
emit("update:response", [...(props.response ?? []), event])
|
||||
|
||||
// TODO: subscription indicator??
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const hideRequestModal = () => {
|
||||
showSaveRequestModal.value = false
|
||||
}
|
||||
|
||||
const prettifyQuery = () => {
|
||||
try {
|
||||
gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
|
||||
prettifyQueryIcon.value = IconCheck
|
||||
} catch (e) {
|
||||
toast.error(`${t("error.gql_prettify_invalid_query")}`)
|
||||
prettifyQueryIcon.value = IconInfo
|
||||
}
|
||||
}
|
||||
|
||||
const saveRequest = () => {
|
||||
showSaveRequestModal.value = true
|
||||
}
|
||||
|
||||
const copyVariables = () => {
|
||||
copyToClipboard(variableString.value)
|
||||
copyVariablesIcon.value = IconCheck
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const prettifyVariableString = () => {
|
||||
try {
|
||||
const jsonObj = JSON.parse(variableString.value)
|
||||
variableString.value = JSON.stringify(jsonObj, null, 2)
|
||||
prettifyVariablesIcon.value = IconCheck
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
prettifyVariablesIcon.value = IconInfo
|
||||
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
||||
}
|
||||
}
|
||||
|
||||
const clearGQLQuery = () => {
|
||||
gqlQueryString.value = ""
|
||||
request.value.query = ""
|
||||
}
|
||||
|
||||
const clearGQLVariables = () => {
|
||||
variableString.value = ""
|
||||
}
|
||||
|
||||
defineActionHandler("request.send-cancel", runQuery)
|
||||
defineActionHandler("request.save", saveRequest)
|
||||
defineActionHandler("request.reset", clearGQLQuery)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<AppPaneLayout layout-id="gql-primary">
|
||||
<template #primary>
|
||||
<GraphqlRequestOptions
|
||||
v-model="tab.document.request"
|
||||
v-model:response="tab.response"
|
||||
:tab-id="tab.id"
|
||||
/>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<GraphqlResponse :response="tab.response" />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { watch } from "vue"
|
||||
import { isEqualHoppGQLRequest } from "~/helpers/graphql"
|
||||
import { HoppGQLTab } from "~/helpers/graphql/tab"
|
||||
|
||||
// TODO: Move Response and Request execution code to over here
|
||||
|
||||
const props = defineProps<{ modelValue: HoppGQLTab }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppGQLTab): void
|
||||
}>()
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
|
||||
// TODO: Come up with a better dirty check
|
||||
let oldRequest = cloneDeep(tab.value.document.request)
|
||||
watch(
|
||||
() => tab.value.document.request,
|
||||
(updatedValue) => {
|
||||
// TODO: Check equality of request
|
||||
if (
|
||||
!tab.value.document.isDirty &&
|
||||
!isEqualHoppGQLRequest(oldRequest, updatedValue)
|
||||
) {
|
||||
tab.value.document.isDirty = true
|
||||
}
|
||||
|
||||
oldRequest = cloneDeep(updatedValue)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
@@ -1,14 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
|
||||
<HoppSmartPlaceholder
|
||||
v-if="responseString === 'loading'"
|
||||
:text="t('state.loading')"
|
||||
>
|
||||
<template #icon>
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="responseString" class="flex flex-col flex-1">
|
||||
<div v-if="response?.length === 1" class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
|
||||
>
|
||||
@@ -37,12 +29,18 @@
|
||||
'action.copy'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
|
||||
:icon="copyResponseIcon"
|
||||
@click="copyResponse"
|
||||
@click="copyResponse(response[0].data)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="schemaEditor" class="flex flex-col flex-1"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="response && response?.length > 1"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<GraphqlSubscriptionLog :log="response" />
|
||||
</div>
|
||||
<AppShortcutsPrompt v-else class="p-4" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,22 +50,34 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import { reactive, ref } from "vue"
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { gqlResponse$ } from "~/newstore/GQLSession"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const responseString = useReadonlyStream(gqlResponse$, "")
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
response: GQLResponseEvent[] | null
|
||||
}>(),
|
||||
{
|
||||
response: null,
|
||||
}
|
||||
)
|
||||
|
||||
const responseString = computed(() => {
|
||||
if (props.response?.length === 1) {
|
||||
return JSON.stringify(JSON.parse(props.response[0].data), null, 2)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
const schemaEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
@@ -95,14 +105,14 @@ const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
1000
|
||||
)
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseString.value!)
|
||||
const copyResponse = (str: string) => {
|
||||
copyToClipboard(str)
|
||||
copyResponseIcon.value = IconCheck
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseString.value
|
||||
const downloadResponse = (str: string) => {
|
||||
const dataToWrite = str
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
@@ -118,6 +128,10 @@ const downloadResponse = () => {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
defineActionHandler("response.file.download", () => downloadResponse())
|
||||
defineActionHandler("response.copy", () => copyResponse())
|
||||
defineActionHandler("response.file.download", () =>
|
||||
downloadResponse.bind(responseString.value)
|
||||
)
|
||||
defineActionHandler("response.copy", () =>
|
||||
copyResponse.bind(responseString.value)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -5,20 +5,6 @@
|
||||
vertical
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartTab
|
||||
:id="'history'"
|
||||
:icon="IconClock"
|
||||
:label="`${t('tab.history')}`"
|
||||
>
|
||||
<History :page="'graphql'" @use-history="handleUseHistory" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'collections'"
|
||||
:icon="IconFolder"
|
||||
:label="`${t('tab.collections')}`"
|
||||
>
|
||||
<CollectionsGraphql />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'docs'"
|
||||
:icon="IconBookOpen"
|
||||
@@ -173,6 +159,21 @@
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab
|
||||
:id="'collections'"
|
||||
:icon="IconFolder"
|
||||
:label="`${t('tab.collections')}`"
|
||||
>
|
||||
<CollectionsGraphql />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'history'"
|
||||
:icon="IconClock"
|
||||
:label="`${t('tab.history')}`"
|
||||
>
|
||||
<History :page="'graphql'" />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
@@ -188,29 +189,24 @@ import IconCopy from "~icons/lucide/copy"
|
||||
import IconBox from "~icons/lucide/box"
|
||||
import { computed, nextTick, reactive, ref } from "vue"
|
||||
import { GraphQLField, GraphQLType } from "graphql"
|
||||
import { map } from "rxjs/operators"
|
||||
import { GQLHeader } from "@hoppscotch/data"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { GQLConnection } from "@helpers/GQLConnection"
|
||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import {
|
||||
setGQLAuth,
|
||||
setGQLHeaders,
|
||||
setGQLQuery,
|
||||
setGQLResponse,
|
||||
setGQLURL,
|
||||
setGQLVariables,
|
||||
} from "~/newstore/GQLSession"
|
||||
graphqlTypes,
|
||||
mutationFields,
|
||||
queryFields,
|
||||
schemaString,
|
||||
subscriptionFields,
|
||||
} from "~/helpers/graphql/connection"
|
||||
|
||||
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
||||
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
||||
|
||||
const selectedNavigationTab = ref<NavigationTabs>("history")
|
||||
const selectedNavigationTab = ref<NavigationTabs>("docs")
|
||||
const selectedGqlTab = ref<GqlTabs>("queries")
|
||||
|
||||
const t = useI18n()
|
||||
@@ -270,40 +266,8 @@ function resolveRootType(type: GraphQLType) {
|
||||
return t
|
||||
}
|
||||
|
||||
type GQLHistoryEntry = {
|
||||
url: string
|
||||
headers: GQLHeader[]
|
||||
query: string
|
||||
response: string
|
||||
variables: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
conn: GQLConnection
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const queryFields = useReadonlyStream(
|
||||
props.conn.queryFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const mutationFields = useReadonlyStream(
|
||||
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const subscriptionFields = useReadonlyStream(
|
||||
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const graphqlTypes = useReadonlyStream(
|
||||
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>(
|
||||
IconDownload,
|
||||
1000
|
||||
@@ -390,11 +354,6 @@ const handleJumpToType = async (type: GraphQLType) => {
|
||||
}
|
||||
}
|
||||
|
||||
const schemaString = useReadonlyStream(
|
||||
props.conn.schemaString$.pipe(map((x) => x ?? "")),
|
||||
""
|
||||
)
|
||||
|
||||
const schemaEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
@@ -436,23 +395,4 @@ const copySchema = () => {
|
||||
copyToClipboard(schemaString.value)
|
||||
copySchemaIcon.value = IconCheck
|
||||
}
|
||||
|
||||
const handleUseHistory = (entry: GQLHistoryEntry) => {
|
||||
const url = entry.url
|
||||
const headers = entry.headers
|
||||
const gqlQueryString = entry.query
|
||||
const variableString = entry.variables
|
||||
const responseText = entry.response
|
||||
|
||||
setGQLURL(url)
|
||||
setGQLHeaders(headers)
|
||||
setGQLQuery(gqlQueryString)
|
||||
setGQLVariables(variableString)
|
||||
setGQLResponse(responseText)
|
||||
setGQLAuth({
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
})
|
||||
props.conn.reset()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div ref="container" class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky top-0 z-10 flex items-center justify-between flex-none pl-4 border-b bg-primary border-dividerLight"
|
||||
>
|
||||
<label for="log" class="py-2 font-semibold text-secondaryLight">
|
||||
{{ "Subscription Log" }}
|
||||
</label>
|
||||
<div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.delete')"
|
||||
:icon="IconTrash"
|
||||
@click="emit('delete')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
id="bottompage"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.scroll_to_top')"
|
||||
:icon="IconArrowUp"
|
||||
@click="scrollTo('top')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
id="bottompage"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.scroll_to_bottom')"
|
||||
:icon="IconArrowDown"
|
||||
@click="scrollTo('bottom')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
id="bottompage"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.autoscroll')"
|
||||
:icon="IconChevronsDown"
|
||||
:class="toggleAutoscrollColor"
|
||||
@click="toggleAutoscroll()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="log.length !== 0"
|
||||
ref="logs"
|
||||
class="overflow-y-auto border-b border-dividerLight"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
|
||||
>
|
||||
<RealtimeLogEntry
|
||||
v-for="(entry, index) in log"
|
||||
:key="`entry-${index}`"
|
||||
:is-open="log.length - 1 === index"
|
||||
:entry="{ ts: entry.time, source: 'info', payload: entry.data }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, PropType, computed, watch, Ref } from "vue"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconArrowUp from "~icons/lucide/arrow-up"
|
||||
import IconArrowDown from "~icons/lucide/arrow-down"
|
||||
import IconChevronsDown from "~icons/lucide/chevron-down"
|
||||
import { useThrottleFn, useScroll } from "@vueuse/core"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||
|
||||
const props = defineProps({
|
||||
log: { type: Array as PropType<GQLResponseEvent[]>, default: () => [] },
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete"): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const logs = ref<HTMLElement | null>(null)
|
||||
|
||||
const autoScrollEnabled = ref(true)
|
||||
|
||||
const logListScroll = useScroll(logs as Ref<HTMLElement>)
|
||||
|
||||
// Disable autoscroll when scrolling to top
|
||||
watch(logListScroll.isScrolling, (isScrolling) => {
|
||||
if (isScrolling && logListScroll.directions.top)
|
||||
autoScrollEnabled.value = false
|
||||
})
|
||||
|
||||
const scrollTo = (position: "top" | "bottom") => {
|
||||
if (position === "top") {
|
||||
logs.value?.scroll({
|
||||
behavior: "smooth",
|
||||
top: 0,
|
||||
})
|
||||
} else if (position === "bottom") {
|
||||
logs.value?.scroll({
|
||||
behavior: "smooth",
|
||||
top: logs.value?.scrollHeight,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.log,
|
||||
useThrottleFn(() => {
|
||||
if (autoScrollEnabled.value) scrollTo("bottom")
|
||||
}, 200),
|
||||
{ flush: "post" }
|
||||
)
|
||||
|
||||
const toggleAutoscroll = () => {
|
||||
autoScrollEnabled.value = !autoScrollEnabled.value
|
||||
}
|
||||
|
||||
const toggleAutoscrollColor = computed(() =>
|
||||
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
|
||||
)
|
||||
</script>
|
||||
@@ -55,51 +55,48 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// TODO: TypeScript + Setup Script this at some point :)
|
||||
|
||||
import { defineComponent } from "vue"
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
GraphQLEnumType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInterfaceType,
|
||||
} from "graphql"
|
||||
import { computed } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop, vue/require-prop-types
|
||||
gqlType: {},
|
||||
gqlTypes: { type: Array, default: () => [] },
|
||||
jumpTypeCallback: { type: Function, default: () => ({}) },
|
||||
isHighlighted: { type: Boolean, default: false },
|
||||
highlightedFields: { type: Array, default: () => [] },
|
||||
},
|
||||
computed: {
|
||||
isInput() {
|
||||
return this.gqlType instanceof GraphQLInputObjectType
|
||||
},
|
||||
isInterface() {
|
||||
return this.gqlType instanceof GraphQLInterfaceType
|
||||
},
|
||||
isEnum() {
|
||||
return this.gqlType instanceof GraphQLEnumType
|
||||
},
|
||||
interfaces() {
|
||||
return (this.gqlType.getInterfaces && this.gqlType.getInterfaces()) || []
|
||||
},
|
||||
children() {
|
||||
return this.gqlTypes.filter(
|
||||
(type) =>
|
||||
type.getInterfaces && type.getInterfaces().includes(this.gqlType)
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isFieldHighlighted({ field }) {
|
||||
return !!this.highlightedFields.find(({ name }) => name === field.name)
|
||||
},
|
||||
const props = defineProps({
|
||||
gqlType: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
gqlTypes: { type: Array, default: () => [] },
|
||||
jumpTypeCallback: { type: Function, default: () => ({}) },
|
||||
isHighlighted: { type: Boolean, default: false },
|
||||
highlightedFields: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const isInput = computed(() => {
|
||||
return props.gqlType instanceof GraphQLInputObjectType
|
||||
})
|
||||
|
||||
const isInterface = computed(() => {
|
||||
return props.gqlType instanceof GraphQLInterfaceType
|
||||
})
|
||||
const isEnum = computed(() => {
|
||||
return props.gqlType instanceof GraphQLEnumType
|
||||
})
|
||||
const interfaces = computed(() => {
|
||||
return (props.gqlType.getInterfaces && props.gqlType.getInterfaces()) || []
|
||||
})
|
||||
|
||||
const children = computed(() => {
|
||||
return props.gqlTypes.filter(
|
||||
(type) => type.getInterfaces && type.getInterfaces().includes(props.gqlType)
|
||||
)
|
||||
})
|
||||
|
||||
const isFieldHighlighted = ({ field }) => {
|
||||
return !!props.highlightedFields.find(({ name }) => name === field.name)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
172
packages/hoppscotch-common/src/components/graphql/Variable.vue
Normal file
172
packages/hoppscotch-common/src/components/graphql/Variable.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("request.variables") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-if="subscriptionState === 'SUBSCRIBED'"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
allowHTML: true,
|
||||
}"
|
||||
:title="`${t('request.stop')}`"
|
||||
:label="`${t('request.stop')}`"
|
||||
:icon="IconStop"
|
||||
class="rounded-none !text-accent !hover:text-accentDark"
|
||||
@click="unsubscribe()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="selectedOperation && subscriptionState !== 'SUBSCRIBED'"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
delay: [500, 20],
|
||||
allowHTML: true,
|
||||
}"
|
||||
:title="`${t('request.run')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
|
||||
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
|
||||
:icon="IconPlay"
|
||||
:disabled="!selectedOperation"
|
||||
class="rounded-none !text-accent !hover:text-accentDark"
|
||||
@click="runQuery(selectedOperation)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearGQLVariables()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.prettify')"
|
||||
:icon="prettifyVariablesIcon"
|
||||
@click="prettifyVariableString"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyVariablesIcon"
|
||||
@click="copyVariables"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="variableEditor" class="flex flex-col flex-1"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
import IconStop from "~icons/lucide/stop-circle"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconWand from "~icons/lucide/wand"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import jsonLinter from "~/helpers/editor/linting/json"
|
||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import * as gql from "graphql"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import {
|
||||
socketDisconnect,
|
||||
subscriptionState,
|
||||
} from "~/helpers/graphql/connection"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save-request"): void
|
||||
(e: "update:modelValue", val: string): void
|
||||
(e: "run-query", definition: gql.OperationDefinitionNode | null): void
|
||||
}>()
|
||||
|
||||
// Watch operations on graphql query string
|
||||
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
||||
|
||||
const variableString = useVModel(props, "modelValue", emit)
|
||||
|
||||
const variableEditor = ref<any | null>(null)
|
||||
|
||||
const linewrapEnabled = ref(false)
|
||||
|
||||
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
const prettifyVariablesIcon = refAutoReset<
|
||||
typeof IconWand | typeof IconCheck | typeof IconInfo
|
||||
>(IconWand, 1000)
|
||||
|
||||
useCodemirror(
|
||||
variableEditor,
|
||||
variableString,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
placeholder: `${t("request.variables")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: computed(() =>
|
||||
variableString.value.length > 0 ? jsonLinter : null
|
||||
),
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
const copyVariables = () => {
|
||||
copyToClipboard(variableString.value)
|
||||
copyVariablesIcon.value = IconCheck
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const prettifyVariableString = () => {
|
||||
try {
|
||||
const jsonObj = JSON.parse(variableString.value)
|
||||
variableString.value = JSON.stringify(jsonObj, null, 2)
|
||||
prettifyVariablesIcon.value = IconCheck
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
prettifyVariablesIcon.value = IconInfo
|
||||
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
||||
}
|
||||
}
|
||||
|
||||
const clearGQLVariables = () => {
|
||||
variableString.value = ""
|
||||
}
|
||||
|
||||
const runQuery = (definition: gql.OperationDefinitionNode | null = null) => {
|
||||
emit("run-query", definition)
|
||||
}
|
||||
const unsubscribe = () => {
|
||||
socketDisconnect()
|
||||
}
|
||||
</script>
|
||||
@@ -56,9 +56,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { makeGQLRequest } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { setGQLSession } from "~/newstore/GQLSession"
|
||||
import { GQLHistoryEntry } from "~/newstore/history"
|
||||
import { shortDateTime } from "~/helpers/utils/date"
|
||||
|
||||
@@ -69,6 +66,8 @@ import IconMinimize2 from "~icons/lucide/minimize-2"
|
||||
import IconMaximize2 from "~icons/lucide/maximize-2"
|
||||
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { makeGQLRequest } from "@hoppscotch/data"
|
||||
import { createNewTab } from "~/helpers/graphql/tab"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -94,19 +93,16 @@ const query = computed(() =>
|
||||
)
|
||||
|
||||
const useEntry = () => {
|
||||
setGQLSession({
|
||||
request: cloneDeep(
|
||||
makeGQLRequest({
|
||||
name: props.entry.request.name,
|
||||
url: props.entry.request.url,
|
||||
headers: props.entry.request.headers,
|
||||
query: props.entry.request.query,
|
||||
variables: props.entry.request.variables,
|
||||
auth: props.entry.request.auth,
|
||||
})
|
||||
),
|
||||
schema: "",
|
||||
response: props.entry.response,
|
||||
createNewTab({
|
||||
request: makeGQLRequest({
|
||||
name: props.entry.request.name,
|
||||
url: props.entry.request.url,
|
||||
headers: props.entry.request.headers,
|
||||
query: props.entry.request.query,
|
||||
variables: props.entry.request.variables,
|
||||
auth: props.entry.request.auth,
|
||||
}),
|
||||
isDirty: false,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
@toggle-star="toggleStar(entry.entry)"
|
||||
@delete-entry="deleteHistory(entry.entry)"
|
||||
@use-entry="useHistory(toRaw(entry.entry))"
|
||||
@add-to-collection="addToCollection(entry.entry)"
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
@@ -176,7 +177,7 @@ import {
|
||||
import HistoryRestCard from "./rest/Card.vue"
|
||||
import HistoryGraphqlCard from "./graphql/Card.vue"
|
||||
import { createNewTab } from "~/helpers/rest/tab"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
|
||||
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
||||
|
||||
@@ -324,6 +325,14 @@ const deleteHistory = (entry: HistoryEntry) => {
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
const addToCollection = (entry: HistoryEntry) => {
|
||||
if (props.page === "rest") {
|
||||
invokeAction("request.save-as", {
|
||||
request: entry.request,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toggleStar = (entry: HistoryEntry) => {
|
||||
// History entry type specified because function does not know the type
|
||||
if (props.page === "rest")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="flex items-stretch group">
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
@contextmenu.prevent="options!.tippy.show()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
||||
@@ -26,6 +29,39 @@
|
||||
{{ entry.request.endpoint }}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
role="menu"
|
||||
@keyup.s="addToCollectionAction?.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
ref="addToCollectionAction"
|
||||
:icon="IconSave"
|
||||
:label="`${t('collection.save_to_collection')}`"
|
||||
:shortcut="['S']"
|
||||
@click="
|
||||
() => {
|
||||
emit('add-to-collection')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconTrash"
|
||||
@@ -48,15 +84,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { computed, ref } from "vue"
|
||||
import findStatusGroup from "~/helpers/findStatusGroup"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { RESTHistoryEntry } from "~/newstore/history"
|
||||
import { shortDateTime } from "~/helpers/utils/date"
|
||||
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import IconStar from "~icons/lucide/star"
|
||||
import IconStarOff from "~icons/hopp/star-off"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
|
||||
const props = defineProps<{
|
||||
entry: RESTHistoryEntry
|
||||
@@ -67,8 +104,13 @@ const emit = defineEmits<{
|
||||
(e: "use-entry"): void
|
||||
(e: "delete-entry"): void
|
||||
(e: "toggle-star"): void
|
||||
(e: "add-to-collection"): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const addToCollectionAction = ref<HTMLButtonElement | null>(null)
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const duration = computed(() => {
|
||||
|
||||
@@ -126,7 +126,6 @@
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
>
|
||||
<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="
|
||||
() => {
|
||||
@@ -115,7 +117,6 @@
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,6 @@
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
:icon="IconPlus"
|
||||
class="mb-4"
|
||||
@click="addBodyParam"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
|
||||
@@ -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,12 +57,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
!(
|
||||
filteredCodegenDefinitions.length !== 0 ||
|
||||
CodegenDefinitions.length === 0
|
||||
)
|
||||
"
|
||||
v-if="filteredCodegenDefinitions.length === 0"
|
||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
||||
>
|
||||
<template #icon>
|
||||
|
||||
@@ -79,16 +79,13 @@
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<HoppSmartAutoComplete
|
||||
<SmartEnvInput
|
||||
v-model="header.key"
|
||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||
:source="commonHeaders"
|
||||
:spellcheck="false"
|
||||
:value="header.key"
|
||||
autofocus
|
||||
styles=" bg-transparent flex flex-1
|
||||
py-1 px-4 truncate "
|
||||
class="flex-1 !flex"
|
||||
@input="
|
||||
:auto-complete-source="commonHeaders"
|
||||
:env-index="index"
|
||||
:inspection-results="getInspectorResult(headerKeyResults, index)"
|
||||
@change="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
key: $event,
|
||||
@@ -100,6 +97,10 @@
|
||||
<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,
|
||||
@@ -212,7 +213,6 @@
|
||||
filled
|
||||
:label="`${t('add.new')}`"
|
||||
:icon="IconPlus"
|
||||
class="mb-4"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -265,6 +265,9 @@ 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()
|
||||
@@ -502,4 +505,39 @@ 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>
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
|
||||
import {
|
||||
HoppGQLAuthOAuth2,
|
||||
HoppRESTAuthOAuth2,
|
||||
parseTemplateString,
|
||||
} from "@hoppscotch/data"
|
||||
import { pluckRef } from "@composables/ref"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
@@ -44,7 +48,7 @@ const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppRESTAuthOAuth2
|
||||
modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
<SmartEnvInput
|
||||
v-model="param.key"
|
||||
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
||||
:inspection-results="
|
||||
getInspectorResult(parameterKeyResults, index)
|
||||
"
|
||||
@change="
|
||||
updateParam(index, {
|
||||
id: param.id,
|
||||
@@ -94,6 +97,9 @@
|
||||
<SmartEnvInput
|
||||
v-model="param.value"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:inspection-results="
|
||||
getInspectorResult(parameterValueResults, index)
|
||||
"
|
||||
@change="
|
||||
updateParam(index, {
|
||||
id: param.id,
|
||||
@@ -155,7 +161,6 @@
|
||||
:label="`${t('add.new')}`"
|
||||
:icon="IconPlus"
|
||||
filled
|
||||
class="mb-4"
|
||||
@click="addParam"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
@@ -173,7 +178,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 { reactive, ref, watch } from "vue"
|
||||
import { computed, 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"
|
||||
@@ -195,6 +200,9 @@ 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()
|
||||
|
||||
@@ -398,4 +406,39 @@ 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>
|
||||
|
||||
@@ -53,16 +53,25 @@
|
||||
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()"
|
||||
@@ -221,6 +230,7 @@
|
||||
v-if="showSaveRequestModal"
|
||||
mode="rest"
|
||||
:show="showSaveRequestModal"
|
||||
:request="request"
|
||||
@hide-modal="showSaveRequestModal = false"
|
||||
/>
|
||||
</div>
|
||||
@@ -233,17 +243,12 @@ 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 { isLeft, isRight } from "fp-ts/lib/Either"
|
||||
import { computed, onBeforeUnmount, ref } from "vue"
|
||||
import { Ref, 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"
|
||||
@@ -258,13 +263,17 @@ 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 } from "~/helpers/rest/tab"
|
||||
import { HoppRESTTab, currentTabID } from "~/helpers/rest/tab"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
||||
import { platform } from "~/platform"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
|
||||
const t = useI18n()
|
||||
const interceptorService = useService(InterceptorService)
|
||||
|
||||
const methods = [
|
||||
"GET",
|
||||
@@ -317,6 +326,8 @@ 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)
|
||||
})
|
||||
@@ -335,13 +346,15 @@ const newSendRequest = async () => {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "rest",
|
||||
strategy: getCurrentStrategyID(),
|
||||
strategy: interceptorService.currentInterceptorID.value!,
|
||||
})
|
||||
|
||||
// Double calling is because the function returns a TaskEither than should be executed
|
||||
const streamResult = await runRESTRequest$(tab)()
|
||||
const [cancel, streamPromise] = runRESTRequest$(tab)
|
||||
const streamResult = await streamPromise
|
||||
|
||||
if (isRight(streamResult)) {
|
||||
requestCancelFunc.value = cancel
|
||||
|
||||
if (E.isRight(streamResult)) {
|
||||
subscribeToStream(
|
||||
streamResult.right,
|
||||
(responseState) => {
|
||||
@@ -358,7 +371,7 @@ const newSendRequest = async () => {
|
||||
loading.value = false
|
||||
}
|
||||
)
|
||||
} else if (isLeft(streamResult)) {
|
||||
} else {
|
||||
loading.value = false
|
||||
toast.error(`${t("error.script_fail")}`)
|
||||
let error: Error
|
||||
@@ -408,9 +421,8 @@ function isCURL(curl: string) {
|
||||
|
||||
const cancelRequest = () => {
|
||||
loading.value = false
|
||||
if (hasExtensionInstalled()) {
|
||||
cancelRunningExtensionRequest()
|
||||
}
|
||||
requestCancelFunc.value?.()
|
||||
|
||||
updateRESTResponse(null)
|
||||
}
|
||||
|
||||
@@ -578,6 +590,8 @@ const saveRequest = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const request = ref<HoppRESTRequest | null>(null)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (loading.value) cancelRequest()
|
||||
})
|
||||
@@ -593,7 +607,22 @@ defineActionHandler("request.method.prev", cycleUpMethod)
|
||||
defineActionHandler("request.save", saveRequest)
|
||||
defineActionHandler(
|
||||
"request.save-as",
|
||||
() => (showSaveRequestModal.value = true)
|
||||
(
|
||||
req:
|
||||
| {
|
||||
requestType: "rest"
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
| {
|
||||
requestType: "gql"
|
||||
request: HoppGQLRequest
|
||||
}
|
||||
) => {
|
||||
showSaveRequestModal.value = true
|
||||
if (req && req.requestType === "rest") {
|
||||
request.value = req.request
|
||||
}
|
||||
}
|
||||
)
|
||||
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
||||
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
||||
@@ -601,6 +630,13 @@ 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" ||
|
||||
@@ -609,4 +645,12 @@ 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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedRealtimeTab"
|
||||
v-model="selectedOptionsTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
@@ -56,12 +56,15 @@ 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()
|
||||
|
||||
@@ -73,10 +76,10 @@ const emit = defineEmits<{
|
||||
|
||||
const request = useVModel(props, "modelValue", emit)
|
||||
|
||||
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
||||
const selectedOptionsTab = ref<RequestOptionTabs>("params")
|
||||
|
||||
const changeTab = (e: RequestOptionTabs) => {
|
||||
selectedRealtimeTab.value = e
|
||||
selectedOptionsTab.value = e
|
||||
}
|
||||
|
||||
const newActiveParamsCount$ = computed(() => {
|
||||
@@ -96,4 +99,8 @@ const newActiveHeadersCount$ = computed(() => {
|
||||
if (e === 0) return null
|
||||
return `${e}`
|
||||
})
|
||||
|
||||
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||
selectedOptionsTab.value = tab
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col flex-1 relative">
|
||||
<HttpResponseMeta :response="tab.response" />
|
||||
<LensesResponseBodyRenderer
|
||||
v-if="!loading && hasResponse"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<AppShortcutsPrompt v-if="response == null" class="flex-1" />
|
||||
<div v-else class="flex flex-col flex-1">
|
||||
@@ -17,6 +17,7 @@
|
||||
: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>
|
||||
@@ -26,6 +27,7 @@
|
||||
: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"
|
||||
@@ -70,6 +72,15 @@
|
||||
</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>
|
||||
|
||||
@@ -80,6 +91,9 @@ 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()
|
||||
@@ -128,4 +142,16 @@ 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>
|
||||
|
||||
126
packages/hoppscotch-common/src/components/http/TabHead.vue
Normal file
126
packages/hoppscotch-common/src/components/http/TabHead.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
:title="tab.document.request.name"
|
||||
class="truncate px-2 flex items-center"
|
||||
@dblclick="emit('open-rename-modal')"
|
||||
@contextmenu.prevent="options?.tippy.show()"
|
||||
@click.middle="emit('close-tab')"
|
||||
>
|
||||
<span
|
||||
class="font-semibold text-tiny"
|
||||
:class="getMethodLabelColorClassOf(tab.document.request)"
|
||||
>
|
||||
{{ tab.document.request.method }}
|
||||
</span>
|
||||
|
||||
<tippy
|
||||
ref="options"
|
||||
trigger="manual"
|
||||
interactive
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span class="leading-8 px-2 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>
|
||||
@@ -153,7 +153,6 @@
|
||||
filled
|
||||
:label="`${t('add.new')}`"
|
||||
:icon="IconPlus"
|
||||
class="mb-4"
|
||||
@click="addUrlEncodedParam"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<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>
|
||||
@@ -131,6 +131,7 @@ const fetchMyTeams = async () => {
|
||||
loading.value = true
|
||||
const result = await runGQLQuery({
|
||||
query: GetMyTeamsDocument,
|
||||
variables: {},
|
||||
})
|
||||
loading.value = false
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, PropType, computed, watch } from "vue"
|
||||
import { ref, PropType, computed, watch, Ref } from "vue"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconArrowUp from "~icons/lucide/arrow-up"
|
||||
import IconArrowDown from "~icons/lucide/arrow-down"
|
||||
@@ -73,7 +73,7 @@ export type LogEntryData = {
|
||||
ts: number | undefined
|
||||
source: "info" | "client" | "server" | "disconnected"
|
||||
payload: string
|
||||
event: "connecting" | "connected" | "disconnected" | "error"
|
||||
event?: "connecting" | "connected" | "disconnected" | "error"
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
@@ -94,7 +94,7 @@ const logs = ref<HTMLElement>()
|
||||
|
||||
const autoScrollEnabled = ref(true)
|
||||
|
||||
const logListScroll = useScroll(logs)
|
||||
const logListScroll = useScroll(logs as Ref<HTMLElement>)
|
||||
|
||||
// Disable autoscroll when scrolling to top
|
||||
watch(logListScroll.isScrolling, (isScrolling) => {
|
||||
|
||||
@@ -209,7 +209,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import * as LJSON from "lossless-json"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { ref, computed, reactive, watch, markRaw } from "vue"
|
||||
import { ref, computed, reactive, watch, markRaw, PropType } from "vue"
|
||||
import { refAutoReset, useTimeAgo } from "@vueuse/core"
|
||||
import { LogEntryData } from "./Log.vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
@@ -227,7 +227,16 @@ import { shortDateTime } from "~/helpers/utils/date"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{ entry: LogEntryData }>()
|
||||
const props = defineProps({
|
||||
entry: {
|
||||
type: Object as PropType<LogEntryData>,
|
||||
required: true,
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
@@ -304,7 +313,7 @@ const outlinePath = computed(() =>
|
||||
)
|
||||
|
||||
// Code for UI Changes
|
||||
const minimized = ref(true)
|
||||
const minimized = ref(props.isOpen ? false : true)
|
||||
watch(minimized, () => {
|
||||
selectedTab.value = isJSON(props.entry.payload) ? "json" : "raw"
|
||||
})
|
||||
@@ -342,7 +351,9 @@ const ENTRY_COLORS = {
|
||||
} as const
|
||||
|
||||
// Assigns color based on entry event
|
||||
const entryColor = computed(() => ENTRY_COLORS[props.entry.event])
|
||||
const entryColor = computed(
|
||||
() => props.entry.event && ENTRY_COLORS[props.entry.event]
|
||||
)
|
||||
|
||||
const ICONS = {
|
||||
info: {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="relative flex flex-col">
|
||||
<input
|
||||
id="selectLabelAdd"
|
||||
v-model="name"
|
||||
v-model="editingName"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
@@ -113,13 +113,13 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const QoS = ref<(typeof QOS_VALUES)[number]>(2)
|
||||
const name = ref("")
|
||||
const editingName = ref("")
|
||||
const color = ref("#f58290")
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
() => {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
QoS.value = 2
|
||||
const randomColor = Math.floor(Math.random() * 16777215).toString(16)
|
||||
color.value = `#${randomColor}`
|
||||
@@ -127,18 +127,18 @@ watch(
|
||||
)
|
||||
|
||||
const addNewSubscription = () => {
|
||||
if (!name.value) {
|
||||
if (!editingName.value) {
|
||||
toastr.error(t("mqtt.invalid_topic").toString())
|
||||
return
|
||||
}
|
||||
emit("submit", {
|
||||
name: name.value,
|
||||
name: editingName.value,
|
||||
qos: QoS.value,
|
||||
color: color.value,
|
||||
})
|
||||
}
|
||||
const hideModal = () => {
|
||||
name.value = ""
|
||||
editingName.value = ""
|
||||
QoS.value = 2
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<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>
|
||||
90
packages/hoppscotch-common/src/components/settings/Proxy.vue
Normal file
90
packages/hoppscotch-common/src/components/settings/Proxy.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<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"
|
||||
:autofocus="false"
|
||||
styles="flex-1"
|
||||
placeholder=" "
|
||||
:label="t('settings.proxy_url')"
|
||||
input-styles="input floating-input"
|
||||
:disabled="!proxyEnabled"
|
||||
/>
|
||||
<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>
|
||||
@@ -18,15 +18,13 @@
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="sticky z-10 top-0 flex-shrink-0 overflow-x-auto">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex w-full p-4 py-2 input !bg-primaryContrast"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
/>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="searchQuery"
|
||||
styles="ticky z-10 top-0 flex-shrink-0 overflow-x-auto"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
type="search"
|
||||
input-styles="flex w-full p-4 py-2 input !bg-primaryContrast"
|
||||
/>
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="autocomplete-wrapper">
|
||||
<div class="absolute inset-0 flex flex-1 overflow-x-auto">
|
||||
<div
|
||||
class="absolute inset-0 flex flex-1 divide-x divide-dividerLight overflow-x-auto"
|
||||
>
|
||||
<div
|
||||
ref="editor"
|
||||
:placeholder="placeholder"
|
||||
@@ -10,6 +12,10 @@
|
||||
@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"
|
||||
@@ -34,8 +40,11 @@
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="suggestions.length === 0" class="pointer-events-none">
|
||||
<span class="truncate py-0.5">
|
||||
{{ t("empty.history_suggestions") }}
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -43,7 +52,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, nextTick, computed, Ref } from "vue"
|
||||
import { ref, onMounted, watch, nextTick, computed, Ref, useSlots } from "vue"
|
||||
import {
|
||||
EditorView,
|
||||
placeholder as placeholderExt,
|
||||
@@ -62,6 +71,7 @@ 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(
|
||||
@@ -75,6 +85,7 @@ const props = withDefaults(
|
||||
environmentHighlights?: boolean
|
||||
readonly?: boolean
|
||||
autoCompleteSource?: string[]
|
||||
inspectionResults?: InspectorResult[] | undefined
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
@@ -85,6 +96,8 @@ const props = withDefaults(
|
||||
readonly: false,
|
||||
environmentHighlights: true,
|
||||
autoCompleteSource: undefined,
|
||||
inspectionResult: undefined,
|
||||
inspectionResults: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -98,6 +111,8 @@ const emit = defineEmits<{
|
||||
(e: "click", ev: any): void
|
||||
}>()
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const cachedValue = ref(props.modelValue)
|
||||
@@ -142,7 +157,9 @@ const suggestions = computed(() => {
|
||||
const updateModelValue = (value: string) => {
|
||||
emit("update:modelValue", value)
|
||||
emit("change", value)
|
||||
showSuggestionPopover.value = false
|
||||
nextTick(() => {
|
||||
showSuggestionPopover.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const handleKeystroke = (ev: KeyboardEvent) => {
|
||||
@@ -342,7 +359,6 @@ const initView = (el: any) => {
|
||||
el.addEventListener("keyup", debounceFn)
|
||||
|
||||
const extensions: Extension = [
|
||||
EditorView.lineWrapping,
|
||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
EditorView.updateListener.of((update) => {
|
||||
@@ -464,7 +480,7 @@ watch(editor, () => {
|
||||
@apply flex;
|
||||
@apply flex-1;
|
||||
@apply flex-shrink-0;
|
||||
@apply whitespace-nowrap;
|
||||
@apply whitespace-nowrap py-4;
|
||||
|
||||
.suggestions {
|
||||
@apply absolute;
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
<template>
|
||||
<HoppSmartModal v-if="show" dialog :title="t('team.new')" @close="hideModal">
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="selectLabelTeamAdd"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="addNewTeam"
|
||||
/>
|
||||
<label for="selectLabelTeamAdd">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
:label="t('action.label')"
|
||||
placeholder=" "
|
||||
input-styles="floating-input"
|
||||
@submit="addNewTeam"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
@@ -58,14 +50,14 @@ const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const name = ref<string | null>(null)
|
||||
const editingName = ref<string | null>(null)
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const addNewTeam = async () => {
|
||||
isLoading.value = true
|
||||
await pipe(
|
||||
TeamNameCodec.decode(name.value),
|
||||
TeamNameCodec.decode(editingName.value),
|
||||
TE.fromEither,
|
||||
TE.mapLeft(() => "invalid_name" as const),
|
||||
TE.chainW(createTeam),
|
||||
@@ -94,7 +86,7 @@ const addNewTeam = async () => {
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
name.value = null
|
||||
editingName.value = null
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
<HoppSmartModal v-if="show" dialog :title="t('team.edit')" @close="hideModal">
|
||||
<template #body>
|
||||
<div class="flex flex-col">
|
||||
<div class="relative flex">
|
||||
<input
|
||||
id="selectLabelTeamEdit"
|
||||
v-model="name"
|
||||
v-focus
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveTeam"
|
||||
/>
|
||||
<label for="selectLabelTeamEdit">
|
||||
{{ t("action.label") }}
|
||||
</label>
|
||||
</div>
|
||||
<HoppSmartInput
|
||||
v-model="editingName"
|
||||
placeholder=" "
|
||||
:label="t('action.label')"
|
||||
input-styles="floating-input"
|
||||
@submit="saveTeam"
|
||||
/>
|
||||
<div class="flex items-center justify-between flex-1 pt-4">
|
||||
<label for="memberList" class="p-4">
|
||||
{{ t("team.members") }}
|
||||
@@ -185,7 +177,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, toRef, watch } from "vue"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import {
|
||||
GetTeamDocument,
|
||||
@@ -236,12 +228,12 @@ const props = defineProps<{
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const name = toRef(props.editingTeam, "name")
|
||||
const editingName = ref(props.editingTeam.name)
|
||||
|
||||
watch(
|
||||
() => props.editingTeam.name,
|
||||
(newName: string) => {
|
||||
name.value = newName
|
||||
editingName.value = newName
|
||||
}
|
||||
)
|
||||
|
||||
@@ -389,11 +381,11 @@ const isLoading = ref(false)
|
||||
|
||||
const saveTeam = async () => {
|
||||
isLoading.value = true
|
||||
if (name.value !== "") {
|
||||
if (TeamNameCodec.is(name.value)) {
|
||||
if (editingName.value !== "") {
|
||||
if (TeamNameCodec.is(editingName.value)) {
|
||||
const updateTeamNameResult = await renameTeam(
|
||||
props.editingTeamID,
|
||||
name.value
|
||||
editingName.value
|
||||
)()
|
||||
if (E.isLeft(updateTeamNameResult)) {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
|
||||
@@ -82,6 +82,7 @@ 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()
|
||||
@@ -154,4 +155,14 @@ 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>
|
||||
|
||||
@@ -58,6 +58,11 @@ type CodeMirrorOptions = {
|
||||
|
||||
// NOTE: This property is not reactive
|
||||
environmentHighlights: boolean
|
||||
|
||||
additionalExts?: Extension[]
|
||||
|
||||
// callback on editor update
|
||||
onUpdate?: (view: ViewUpdate) => void
|
||||
}
|
||||
|
||||
const hoppCompleterExt = (completer: Completer): Extension => {
|
||||
@@ -189,6 +194,7 @@ export function useCodemirror(
|
||||
): { cursor: Ref<{ line: number; ch: number }> } {
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const additionalExts = new Compartment()
|
||||
const language = new Compartment()
|
||||
const lineWrapping = new Compartment()
|
||||
const placeholderConfig = new Compartment()
|
||||
@@ -254,12 +260,24 @@ export function useCodemirror(
|
||||
|
||||
el.addEventListener("mouseup", debounceFn)
|
||||
el.addEventListener("keyup", debounceFn)
|
||||
const cursorPos = update.state.selection.main.head
|
||||
const line = update.state.doc.lineAt(cursorPos)
|
||||
|
||||
cachedCursor.value = {
|
||||
line: line.number - 1,
|
||||
ch: cursorPos - line.from,
|
||||
if (options.onUpdate) {
|
||||
options.onUpdate(update)
|
||||
}
|
||||
|
||||
if (update.selectionSet) {
|
||||
const cursorPos = update.state.selection.main.head
|
||||
const line = update.state.doc.lineAt(cursorPos)
|
||||
|
||||
cachedCursor.value = {
|
||||
line: line.number - 1,
|
||||
ch: cursorPos - line.from,
|
||||
}
|
||||
|
||||
cursor.value = {
|
||||
line: cachedCursor.value.line,
|
||||
ch: cachedCursor.value.ch,
|
||||
}
|
||||
}
|
||||
|
||||
cursor.value = {
|
||||
@@ -313,6 +331,7 @@ export function useCodemirror(
|
||||
},
|
||||
]),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
additionalExts.of(options.additionalExts ?? []),
|
||||
]
|
||||
|
||||
if (environmentTooltip) extensions.push(environmentTooltip.extension)
|
||||
@@ -388,6 +407,15 @@ export function useCodemirror(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => options.additionalExts,
|
||||
(newExts) => {
|
||||
view.value?.dispatch({
|
||||
effects: additionalExts.reconfigure(newExts ?? []),
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => options.extendedEditorConfig.lineWrapping,
|
||||
(newMode) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
parseGQLErrorString,
|
||||
} from "@helpers/backend/GQLClient"
|
||||
import {
|
||||
AnyVariables,
|
||||
createRequest,
|
||||
GraphQLRequest,
|
||||
OperationResult,
|
||||
@@ -35,7 +36,11 @@ type UseQueryOptions<T = any, V = object> = {
|
||||
pollDuration?: number | undefined
|
||||
}
|
||||
|
||||
export const useGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
|
||||
export const useGQLQuery = <
|
||||
DocType,
|
||||
DocVarType extends AnyVariables,
|
||||
DocErrorType extends string,
|
||||
>(
|
||||
_args: UseQueryOptions<DocType, DocVarType>
|
||||
) => {
|
||||
const stops: WatchStopHandle[] = []
|
||||
|
||||
@@ -14,7 +14,7 @@ type CloneMode = "noclone" | "shallow" | "deep"
|
||||
*/
|
||||
export function useReadonlyStream<T>(
|
||||
stream$: Observable<T>,
|
||||
initialValue: T,
|
||||
initialValue?: T,
|
||||
cloneMode: CloneMode = "shallow"
|
||||
): Ref<T> {
|
||||
let sub: Subscription | null = null
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import {
|
||||
getIntrospectionQuery,
|
||||
buildClientSchema,
|
||||
GraphQLSchema,
|
||||
printSchema,
|
||||
GraphQLObjectType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLEnumType,
|
||||
GraphQLInterfaceType,
|
||||
} from "graphql"
|
||||
import { distinctUntilChanged, map } from "rxjs/operators"
|
||||
import { GQLHeader, HoppGQLAuth } from "@hoppscotch/data"
|
||||
import { sendNetworkRequest } from "./network"
|
||||
|
||||
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
||||
|
||||
/**
|
||||
GQLConnection deals with all the operations (like polling, schema extraction) that runs
|
||||
when a connection is made to a GraphQL server.
|
||||
*/
|
||||
export class GQLConnection {
|
||||
public isLoading$ = new BehaviorSubject<boolean>(false)
|
||||
public connected$ = new BehaviorSubject<boolean>(false)
|
||||
public schema$ = new BehaviorSubject<GraphQLSchema | null>(null)
|
||||
|
||||
public schemaString$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
return printSchema(schema, {
|
||||
commentDescriptions: true,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
public queryFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getQueryType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public mutationFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getMutationType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public subscriptionFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getSubscriptionType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public graphqlTypes$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const typeMap = schema.getTypeMap()
|
||||
|
||||
const queryTypeName = schema.getQueryType()?.name ?? ""
|
||||
const mutationTypeName = schema.getMutationType()?.name ?? ""
|
||||
const subscriptionTypeName = schema.getSubscriptionType()?.name ?? ""
|
||||
|
||||
return Object.values(typeMap).filter((type) => {
|
||||
return (
|
||||
!type.name.startsWith("__") &&
|
||||
![queryTypeName, mutationTypeName, subscriptionTypeName].includes(
|
||||
type.name
|
||||
) &&
|
||||
(type instanceof GraphQLObjectType ||
|
||||
type instanceof GraphQLInputObjectType ||
|
||||
type instanceof GraphQLEnumType ||
|
||||
type instanceof GraphQLInterfaceType)
|
||||
)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
private timeoutSubscription: any
|
||||
|
||||
public connect(url: string, headers: GQLHeader[], auth: HoppGQLAuth) {
|
||||
if (this.connected$.value) {
|
||||
throw new Error(
|
||||
"A connection is already running. Close it before starting another."
|
||||
)
|
||||
}
|
||||
|
||||
// Polling
|
||||
this.connected$.next(true)
|
||||
|
||||
const poll = async () => {
|
||||
await this.getSchema(url, headers, auth)
|
||||
this.timeoutSubscription = setTimeout(() => {
|
||||
poll()
|
||||
}, GQL_SCHEMA_POLL_INTERVAL)
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (!this.connected$.value) {
|
||||
throw new Error("No connections are running to be disconnected")
|
||||
}
|
||||
|
||||
clearTimeout(this.timeoutSubscription)
|
||||
this.connected$.next(false)
|
||||
}
|
||||
|
||||
public reset() {
|
||||
if (this.connected$.value) this.disconnect()
|
||||
|
||||
this.isLoading$.next(false)
|
||||
this.connected$.next(false)
|
||||
this.schema$.next(null)
|
||||
}
|
||||
|
||||
private async getSchema(
|
||||
url: string,
|
||||
reqHeaders: GQLHeader[],
|
||||
auth: HoppGQLAuth
|
||||
) {
|
||||
try {
|
||||
this.isLoading$.next(true)
|
||||
|
||||
const introspectionQuery = JSON.stringify({
|
||||
query: getIntrospectionQuery(),
|
||||
})
|
||||
|
||||
const headers = reqHeaders.filter((x) => x.active && x.key !== "")
|
||||
|
||||
// TODO: Support a better b64 implementation than btoa ?
|
||||
if (auth.authType === "basic") {
|
||||
const username = auth.username
|
||||
const password = auth.password
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||
})
|
||||
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${auth.token}`,
|
||||
})
|
||||
} else if (auth.authType === "api-key") {
|
||||
const { key, value, addTo } = auth
|
||||
|
||||
if (addTo === "Headers") {
|
||||
headers.push({
|
||||
active: true,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
headers.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "POST",
|
||||
url,
|
||||
headers: {
|
||||
...finalHeaders,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: introspectionQuery,
|
||||
}
|
||||
|
||||
const data = await sendNetworkRequest(reqOptions)
|
||||
|
||||
// HACK : Temporary trailing null character issue from the extension fix
|
||||
const response = new TextDecoder("utf-8")
|
||||
.decode(data.data)
|
||||
.replace(/\0+$/, "")
|
||||
|
||||
const introspectResponse = JSON.parse(response)
|
||||
|
||||
const schema = buildClientSchema(introspectResponse.data)
|
||||
|
||||
this.schema$.next(schema)
|
||||
|
||||
this.isLoading$.next(false)
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
this.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
public async runQuery(
|
||||
url: string,
|
||||
headers: GQLHeader[],
|
||||
query: string,
|
||||
variables: string,
|
||||
auth: HoppGQLAuth
|
||||
) {
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
|
||||
const parsedVariables = JSON.parse(variables || "{}")
|
||||
|
||||
const params: Record<string, string> = {}
|
||||
|
||||
if (auth.authActive) {
|
||||
if (auth.authType === "basic") {
|
||||
const username = auth.username
|
||||
const password = auth.password
|
||||
finalHeaders.Authorization = `Basic ${btoa(`${username}:${password}`)}`
|
||||
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
|
||||
finalHeaders.Authorization = `Bearer ${auth.token}`
|
||||
} else if (auth.authType === "api-key") {
|
||||
const { key, value, addTo } = auth
|
||||
if (addTo === "Headers") {
|
||||
finalHeaders[key] = value
|
||||
} else if (addTo === "Query params") {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headers
|
||||
.filter((item) => item.active && item.key !== "")
|
||||
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "POST",
|
||||
url,
|
||||
headers: {
|
||||
...finalHeaders,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: JSON.stringify({
|
||||
query,
|
||||
variables: parsedVariables,
|
||||
}),
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
}
|
||||
|
||||
const res = await sendNetworkRequest(reqOptions)
|
||||
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
const responseText = new TextDecoder("utf-8")
|
||||
.decode(res.data)
|
||||
.replace(/\0+$/, "")
|
||||
|
||||
return responseText
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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"
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
runTestScript,
|
||||
TestDescriptor,
|
||||
} from "@hoppscotch/js-sandbox"
|
||||
import { isRight } from "fp-ts/Either"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import {
|
||||
getCombinedEnvVariables,
|
||||
@@ -69,106 +68,130 @@ export const executedResponses$ = new Subject<
|
||||
HoppRESTResponse & { type: "success" | "fail " }
|
||||
>()
|
||||
|
||||
export const runRESTRequest$ = (
|
||||
export function runRESTRequest$(
|
||||
tab: Ref<HoppRESTTab>
|
||||
): 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),
|
||||
}
|
||||
)
|
||||
): [
|
||||
() => void,
|
||||
Promise<
|
||||
| E.Left<"script_fail" | "cancellation">
|
||||
| E.Right<Observable<HoppRESTResponse>>
|
||||
>,
|
||||
] {
|
||||
let cancelCalled = false
|
||||
let cancelFunc: (() => void) | null = null
|
||||
|
||||
const stream = createRESTNetworkRequestStream(effectiveRequest)
|
||||
const cancel = () => {
|
||||
cancelCalled = true
|
||||
cancelFunc?.()
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
const res = getFinalEnvsFromPreRequest(
|
||||
tab.value.document.request.preRequestScript,
|
||||
getCombinedEnvVariables()
|
||||
)().then((envs) => {
|
||||
if (cancelCalled) return E.left("cancellation" as const)
|
||||
|
||||
const runResult = await runTestScript(res.req.testScript, envs, {
|
||||
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,
|
||||
{
|
||||
status: res.statusCode,
|
||||
body: getTestableBody(res),
|
||||
headers: res.headers,
|
||||
})()
|
||||
|
||||
if (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,
|
||||
}
|
||||
)
|
||||
} 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: [],
|
||||
},
|
||||
},
|
||||
scriptError: true,
|
||||
}
|
||||
}
|
||||
)()
|
||||
|
||||
subscription.unsubscribe()
|
||||
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,
|
||||
}
|
||||
)
|
||||
} 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: [],
|
||||
},
|
||||
},
|
||||
scriptError: true,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return TE.right(stream)
|
||||
})
|
||||
)
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
})
|
||||
|
||||
return E.right(stream)
|
||||
})
|
||||
|
||||
return [cancel, res]
|
||||
}
|
||||
|
||||
const getAddedEnvVariables = (
|
||||
current: Environment["variables"],
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { HoppRESTDocument } from "./rest/document"
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||
|
||||
export type HoppAction =
|
||||
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
||||
@@ -14,6 +15,7 @@ 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
|
||||
@@ -21,13 +23,22 @@ 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
|
||||
@@ -45,6 +56,7 @@ export type HoppAction =
|
||||
| "history.clear" // Clear REST History
|
||||
| "user.login" // Login to Hoppscotch
|
||||
| "user.logout" // Log out of Hoppscotch
|
||||
| "editor.format" // Format editor content
|
||||
|
||||
/**
|
||||
* Defines the arguments, if present for a given type that is required to be passed on
|
||||
@@ -67,15 +79,34 @@ type HoppActionArgsMap = {
|
||||
}
|
||||
"modals.my.environment.edit": {
|
||||
envName: string
|
||||
variableName: string
|
||||
variableName?: string
|
||||
}
|
||||
"modals.team.environment.edit": {
|
||||
envName: string
|
||||
variableName: string
|
||||
variableName?: string
|
||||
}
|
||||
"modals.team.delete": {
|
||||
teamId: string
|
||||
}
|
||||
"workspace.switch": {
|
||||
teamId: 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
|
||||
}
|
||||
@@ -144,7 +175,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>
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
Operation,
|
||||
OperationResult,
|
||||
Client,
|
||||
AnyVariables,
|
||||
} from "@urql/core"
|
||||
import { authExchange } from "@urql/exchange-auth"
|
||||
import { devtoolsExchange } from "@urql/devtools"
|
||||
import { AuthConfig, 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"
|
||||
@@ -67,43 +68,43 @@ const createSubscriptionClient = () => {
|
||||
|
||||
const createHoppClient = () => {
|
||||
const exchanges = [
|
||||
devtoolsExchange,
|
||||
// devtoolsExchange,
|
||||
dedupExchange,
|
||||
authExchange({
|
||||
addAuthToOperation({ authState, operation }) {
|
||||
if (!authState) {
|
||||
return operation
|
||||
}
|
||||
authExchange(async (): Promise<AuthConfig> => {
|
||||
const probableUser = platform.auth.getProbableUser()
|
||||
if (probableUser !== null)
|
||||
await platform.auth.waitProbableLoginToConfirm()
|
||||
|
||||
const fetchOptions =
|
||||
typeof operation.context.fetchOptions === "function"
|
||||
? operation.context.fetchOptions()
|
||||
: operation.context.fetchOptions || {}
|
||||
return {
|
||||
addAuthToOperation(operation) {
|
||||
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()
|
||||
},
|
||||
getAuth: async () => {
|
||||
const probableUser = platform.auth.getProbableUser()
|
||||
|
||||
if (probableUser !== null)
|
||||
await platform.auth.waitProbableLoginToConfirm()
|
||||
|
||||
return {}
|
||||
},
|
||||
})
|
||||
},
|
||||
willAuthError() {
|
||||
return platform.auth.willBackendHaveAuthError()
|
||||
},
|
||||
didAuthError() {
|
||||
return false
|
||||
},
|
||||
async refreshAuth() {
|
||||
// TODO
|
||||
},
|
||||
}
|
||||
}),
|
||||
fetchExchange,
|
||||
errorExchange({
|
||||
@@ -165,9 +166,9 @@ export function initBackendGQLClient() {
|
||||
})
|
||||
}
|
||||
|
||||
type RunQueryOptions<T = any, V = object> = {
|
||||
type RunQueryOptions<T = any, V = AnyVariables> = {
|
||||
query: TypedDocumentNode<T, V>
|
||||
variables?: V
|
||||
variables: V
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,7 +184,11 @@ export type GQLError<T extends string> =
|
||||
error: T
|
||||
}
|
||||
|
||||
export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
|
||||
export const runGQLQuery = <
|
||||
DocType,
|
||||
DocVarType extends AnyVariables,
|
||||
DocErrorType extends string,
|
||||
>(
|
||||
args: RunQueryOptions<DocType, DocVarType>
|
||||
): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
|
||||
const request = createRequest<DocType, DocVarType>(args.query, args.variables)
|
||||
@@ -245,8 +250,8 @@ export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
|
||||
// Make sure to handle cases if the subscription fires with the same update multiple times
|
||||
export const runGQLSubscription = <
|
||||
DocType,
|
||||
DocVarType,
|
||||
DocErrorType extends string
|
||||
DocVarType extends AnyVariables,
|
||||
DocErrorType extends string,
|
||||
>(
|
||||
args: RunQueryOptions<DocType, DocVarType>
|
||||
) => {
|
||||
@@ -335,10 +340,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(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user