Compare commits

...

22 Commits

Author SHA1 Message Date
nivedin
00588bcc0a fix: interceptor error from extension issue 2023-11-14 17:17:07 +05:30
Akash K
e24d0ce605 fix: oauth 2.0 authentication type is breaking (#3531) 2023-11-14 00:12:04 +05:30
Andrew Bastin
de725337d6 fix: window drag taking precedence on windows 2023-11-08 20:07:13 +05:30
Andrew Bastin
9d1d369f37 fix: performance issues due to mouse on header detection 2023-11-08 18:47:40 +05:30
Andrew Bastin
2bd925d441 chore: correct version of selfhost-desktop 2023-11-08 17:18:12 +05:30
Liyas Thomas
bb8dc6f7eb chore: updated brand assets (#3500)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-11-08 15:47:35 +05:30
Anwarul Islam
be3e5ba7e7 fix: graphql query deprecation issue (#3506) 2023-11-08 14:51:39 +05:30
Andrew Bastin
663134839f feat: let platforms disable we are using cookies prompt 2023-11-07 20:49:07 +05:30
Andrew Bastin
736f83a70c fix: header inspector cookie inspection will not trigger if current interceptor supports cookies 2023-11-07 20:36:34 +05:30
Andrew Bastin
05d2175f43 fix: selfhost-desktop not running gql-codegen 2023-11-07 20:24:22 +05:30
Andrew Bastin
97bd808431 chore: set versions with a bump version 2023-11-07 16:04:02 +05:30
Andrew Bastin
a13c2fd4c1 chore: pin @codemirror/language to 6.9.0 2023-11-07 15:57:19 +05:30
Andrew Bastin
16044b5840 feat: desktop app
Co-authored-by: Vivek R <123vivekr@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-11-07 14:20:03 +05:30
Andrew Bastin
4ebf850cb6 chore: bump version to 2023.8.3 2023-11-06 17:39:31 +05:30
Balu Babu
76af7d5e10 fix: mailer template issue (#3475) 2023-11-06 17:25:36 +05:30
Nicolas Merget
5428a73811 fix: add optional chaining for teamMembers to handle undefined team (#3484)
Co-authored-by: James George <jamesgeorge998001@gmail.com>
2023-11-06 11:25:39 +05:30
Anwarul Islam
4a154e6569 chore: fix spelling mistake on type import (#3487) 2023-11-06 11:25:03 +05:30
Liyas Thomas
0aa5825d8b fix: cleanup ui and improve consistency in input elements (#3494) 2023-11-06 10:56:15 +05:30
Andrew Bastin
bdb63e99d5 fix: pin @lezer/highlight to 1.1.4 to prevent page breaks 2023-11-03 23:30:46 +05:30
James George
8175ec640a chore(data): bump dependencies (#3473) 2023-11-02 23:53:52 +05:30
James George
b5307e4a89 chore(common): implement enforced pre-commit type checks for FE service files (#3472) 2023-11-02 23:37:27 +05:30
Akash K
19294802be fix: graphql page crashing and broken syntax highlighting (#3488) 2023-11-02 23:10:37 +05:30
198 changed files with 13720 additions and 1267 deletions

View File

@@ -17,8 +17,8 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/language": "^6.9.1",
"@lezer/highlight": "^1.1.6",
"@codemirror/language": "6.9.0",
"@lezer/highlight": "1.1.4",
"@lezer/lr": "^1.3.13"
},
"devDependencies": {

View File

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

View File

@@ -74,7 +74,7 @@ export class AdminService {
try {
await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
template: 'code-your-own',
template: 'user-invitation',
variables: {
inviteeEmail: inviteeEmail,
magicLink: `${process.env.VITE_BASE_URL}`,

View File

@@ -229,7 +229,7 @@ export class AuthService {
}
await this.mailerService.sendEmail(email, {
template: 'code-your-own',
template: 'user-invitation',
variables: {
inviteeEmail: email,
magicLink: `${url}/enter?token=${generatedTokens.token}`,

View File

@@ -8,7 +8,7 @@ export type MailDescription = {
};
export type UserMagicLinkMailDescription = {
template: 'code-your-own';
template: 'user-invitation';
variables: {
inviteeEmail: string;
magicLink: string;
@@ -16,7 +16,7 @@ export type UserMagicLinkMailDescription = {
};
export type AdminUserInvitationMailDescription = {
template: 'code-your-own';
template: 'user-invitation';
variables: {
inviteeEmail: string;
magicLink: string;

View File

@@ -27,7 +27,7 @@ export class MailerService {
case 'team-invitation':
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
case 'code-your-own':
case 'user-invitation':
return 'Sign in to Hoppscotch';
}
}

View File

@@ -1,5 +1,6 @@
{
"action": {
"add": "Add",
"autoscroll": "Autoscroll",
"cancel": "Cancel",
"choose_file": "Choose a file",
@@ -54,9 +55,28 @@
"new": "Add new",
"star": "Add star"
},
"cookies": {
"modal": {
"new_domain_name": "New domain name",
"set": "Set a cookie",
"cookie_string": "Cookie string",
"enter_cookie_string": "Enter cookie string",
"cookie_name": "Name",
"cookie_value": "Value",
"cookie_path": "Path",
"cookie_expires": "Expires",
"managed_tab": "Managed",
"raw_tab": "Raw",
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
"empty_domains": "Domain list is empty",
"empty_domain": "Domain is empty",
"no_cookies_in_domain": "No cookies set for this domain"
}
},
"app": {
"chat_with_us": "Chat with us",
"contact_us": "Contact us",
"cookies": "Cookies",
"copy": "Copy",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
@@ -237,6 +257,7 @@
"error": {
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
"check_console_details": "Check console log for details.",
"check_how_to_add_origin": "Check how you can add an origin",
"curl_invalid_format": "cURL is not formatted properly",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
@@ -257,6 +278,7 @@
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"please_install_extension": "Please install the extension and add origin to the extension.",
"proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
@@ -764,7 +786,7 @@
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
"published_message": "Published message: {message} to topic: {topic}",
"reconnection_error": "Failed to reconnect",
"show":"Show",
"show": "Show",
"subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.8.2",
"version": "2023.8.3-1",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -17,7 +17,7 @@
"postinstall": "pnpm run gql-codegen",
"do-test": "pnpm run test",
"do-lint": "pnpm run prod-lint",
"do-typecheck": "pnpm run lint",
"do-typecheck": "node type-check.mjs",
"do-lintfix": "pnpm run lintfix"
},
"dependencies": {
@@ -27,12 +27,12 @@
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.9.1",
"@codemirror/language": "6.9.0",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4",
"@codemirror/state": "^6.3.1",
"@codemirror/view": "^6.21.3",
"@codemirror/view": "^6.22.0",
"@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.9",
@@ -41,7 +41,7 @@
"@hoppscotch/js-sandbox": "workspace:^",
"@hoppscotch/ui": "workspace:^",
"@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "^1.1.6",
"@lezer/highlight": "1.1.4",
"@urql/core": "^4.1.1",
"@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6",
@@ -52,6 +52,7 @@
"acorn-walk": "^8.2.0",
"axios": "^1.4.0",
"buffer": "^6.0.3",
"cookie-es": "^1.0.0",
"dioc": "workspace:^",
"esprima": "^4.0.1",
"events": "^3.3.0",
@@ -76,6 +77,8 @@
"process": "^0.11.10",
"qs": "^6.11.2",
"rxjs": "^7.8.1",
"set-cookie-parser": "^2.6.0",
"set-cookie-parser-es": "^1.0.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
@@ -98,7 +101,8 @@
"wonka": "^6.3.4",
"workbox-window": "^7.0.0",
"xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1"
"yargs-parser": "^21.1.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
@@ -136,6 +140,7 @@
"eslint": "^8.47.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"glob": "^10.3.10",
"npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3",
"rollup-plugin-polyfill-node": "^0.12.0",

View File

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

After

Width:  |  Height:  |  Size: 386 B

View File

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

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -1 +1,50 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none"><path fill="#10B981" d="M0 0h512v512H0z"/><circle cx="197.76" cy="157.84" r="10" fill="#fff" fill-opacity=".75"/><circle cx="259.76" cy="161.84" r="12" fill="#fff" fill-opacity=".75"/><circle cx="319.76" cy="177.84" r="10" fill="#fff" fill-opacity=".75"/><path d="M344.963 235.676c2.075-12.698-38.872-29.804-90.967-38.094-52.09-8.296-96.404-4.665-98.48 8.033-.257 1.035 0 1.812.263 2.853-1.298-.521-76.714 211.212-76.714 211.212H364.14s-17.621-181.414-20.211-181.414c.515-.772 1.035-1.549 1.035-2.59Z" fill="url(#a)"/><path d="M314.902 227.386c-1.298 8.033-30.839 9.845-66.343 4.402-35.247-5.7-62.982-16.843-61.684-24.618.521-2.59 3.888-4.665 9.331-5.7-18.141.777-30.062 4.145-31.096 9.845-1.555 10.628 34.726 25.139 81.373 32.657 46.647 7.512 85.782 4.665 87.594-5.7 1.041-6.226-9.33-12.961-26.431-19.439 4.923 2.847 7.513 5.957 7.256 8.553Z" fill="#A7F3D0" fill-opacity=".5"/><path d="M333.557 157.413c-3.104-32.137-27.729-59.351-60.9-64.53-33.172-5.186-64.531 12.954-77.749 42.238 21.251 1.298 44.057 3.631 67.904 7.518 25.396 3.888 49.237 9.074 70.745 14.774Z" fill="url(#b)"/><path d="M74.142 158.002c-2.59 15.808 30.319 35.247 81.894 51.055-.257-1.04-.257-1.818-.257-2.853 2.07-12.698 46.127-16.328 98.48-8.032 52.347 8.29 93.037 25.396 90.961 38.094-.257 1.04-.514 1.818-1.035 2.589 53.645.778 90.968-7.512 93.557-23.32 3.625-24.104-74.638-56.498-174.93-72.306-100.555-15.808-185.045-9.331-188.67 14.773Zm115.586-1.298c.778-4.145 4.665-7.255 8.81-6.477 4.145.777 7.256 4.665 6.478 8.81-.52 4.145-4.665 6.998-8.81 6.478-4.145-.778-7.255-4.666-6.478-8.811Zm59.866 4.145c.777-5.7 6.22-9.587 11.92-8.547 5.7.778 9.588 6.215 8.553 11.921-1.041 5.442-6.478 9.33-11.92 8.553-5.706-.778-9.594-6.221-8.553-11.927Zm62.975 15.294c.778-4.145 4.665-7.255 8.81-6.478 4.145.778 7.255 4.666 6.478 8.811-.515 4.145-4.665 7.255-8.81 6.477-4.145-.777-7.256-4.665-6.478-8.81Z" fill="url(#c)"/><defs><radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 32.7063 -69.3245 0 264.232 124.706)"><stop stop-color="#047857"/><stop offset="1" stop-color="#064E3B"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(255.837 186.754) scale(1389.61)"><stop stop-color="#047857"/><stop offset=".115" stop-color="#064E3B"/></radialGradient><linearGradient id="a" x1="224.998" y1="157.606" x2="224.998" y2="403.696" gradientUnits="userSpaceOnUse"><stop stop-color="#86EFAC" stop-opacity=".75"/><stop offset=".635" stop-color="#fff" stop-opacity=".2"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
<svg width="824" height="824" viewBox="0 0 824 824" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="824" height="824" rx="184" fill="#08110F"/>
<rect width="824" height="824" rx="184" fill="url(#paint0_radial_0_21)" fill-opacity="0.5"/>
<path d="M435.425 463.217C429.441 476.657 411.033 481.515 394.309 474.07C377.585 466.624 368.879 449.693 374.863 436.253C380.846 422.813 399.254 417.954 415.978 425.4C432.702 432.846 441.409 449.777 435.425 463.217Z" fill="url(#paint1_linear_0_21)"/>
<path d="M435.425 463.217C429.441 476.657 411.033 481.515 394.309 474.07C377.585 466.624 368.879 449.693 374.863 436.253C380.846 422.813 399.254 417.954 415.978 425.4C432.702 432.846 441.409 449.777 435.425 463.217Z" fill="url(#paint2_radial_0_21)" style="mix-blend-mode:soft-light"/>
<path d="M535.563 521.172C553.071 526.191 570.536 518.856 574.571 504.789C578.606 490.722 567.684 475.251 550.175 470.232C532.666 465.213 515.201 472.548 511.166 486.615C507.131 500.682 518.054 516.153 535.563 521.172Z" fill="url(#paint3_linear_0_21)"/>
<path d="M535.563 521.172C553.071 526.191 570.536 518.856 574.571 504.789C578.606 490.722 567.684 475.251 550.175 470.232C532.666 465.213 515.201 472.548 511.166 486.615C507.131 500.682 518.054 516.153 535.563 521.172Z" fill="url(#paint4_radial_0_21)" style="mix-blend-mode:soft-light"/>
<path d="M292.782 355.633C308.227 365.286 314.462 383.173 306.709 395.584C298.955 407.995 280.149 410.231 264.704 400.578C249.258 390.924 243.023 373.037 250.777 360.626C258.53 348.215 277.337 345.98 292.782 355.633Z" fill="url(#paint5_linear_0_21)"/>
<path d="M292.782 355.633C308.227 365.286 314.462 383.173 306.709 395.584C298.955 407.995 280.149 410.231 264.704 400.578C249.258 390.924 243.023 373.037 250.777 360.626C258.53 348.215 277.337 345.98 292.782 355.633Z" fill="url(#paint6_radial_0_21)" style="mix-blend-mode:soft-light"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M502.355 231.325C581.373 266.506 632.095 343.263 634.119 429.03C680.633 465.639 726.858 516.883 705.36 565.168C681.25 619.319 595.382 617.091 497.781 589.689C450.767 615.718 392.444 620.168 339.689 596.68C286.934 573.192 251.229 526.908 239.1 474.517C153.428 420.321 94.3151 357.999 118.425 303.847C139.923 255.562 208.935 255.626 267.265 265.697C332.356 209.81 423.338 196.144 502.355 231.325ZM159.38 322.082C147.667 348.389 210.578 423.052 382.845 499.751C555.111 576.449 652.693 573.241 664.405 546.934C674.099 525.16 634.213 483.308 588.537 450.878C553.009 425.484 504.344 397.494 440.864 369.231C423.586 361.538 416.839 341.008 424.104 324.691C431.369 308.374 447.329 297.463 480.93 295.91C496.747 295.862 498.823 291.476 499.546 287.716C500.442 281.915 492.401 276.002 484.108 272.31C418.17 242.953 337.453 255.265 281.503 314.178C226.84 301.933 169.074 300.309 159.38 322.082Z" fill="url(#paint7_linear_0_21)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M502.355 231.325C581.373 266.506 632.095 343.263 634.119 429.03C680.633 465.639 726.858 516.883 705.36 565.168C681.25 619.319 595.382 617.091 497.781 589.689C450.767 615.718 392.444 620.168 339.689 596.68C286.934 573.192 251.229 526.908 239.1 474.517C153.428 420.321 94.3151 357.999 118.425 303.847C139.923 255.562 208.935 255.626 267.265 265.697C332.356 209.81 423.338 196.144 502.355 231.325ZM159.38 322.082C147.667 348.389 210.578 423.052 382.845 499.751C555.111 576.449 652.693 573.241 664.405 546.934C674.099 525.16 634.213 483.308 588.537 450.878C553.009 425.484 504.344 397.494 440.864 369.231C423.586 361.538 416.839 341.008 424.104 324.691C431.369 308.374 447.329 297.463 480.93 295.91C496.747 295.862 498.823 291.476 499.546 287.716C500.442 281.915 492.401 276.002 484.108 272.31C418.17 242.953 337.453 255.265 281.503 314.178C226.84 301.933 169.074 300.309 159.38 322.082Z" fill="url(#paint8_radial_0_21)" style="mix-blend-mode:soft-light"/>
<defs>
<radialGradient id="paint0_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(814.524 12.36) rotate(125.613) scale(1089.59 1210.34)">
<stop stop-color="#00D196" stop-opacity="0.5"/>
<stop offset="0.996771" stop-color="#00D196" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint1_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D196"/>
<stop offset="1" stop-color="#00B381"/>
</linearGradient>
<radialGradient id="paint2_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint3_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D196"/>
<stop offset="1" stop-color="#00B381"/>
</linearGradient>
<radialGradient id="paint4_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint5_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D196"/>
<stop offset="1" stop-color="#00B381"/>
</linearGradient>
<radialGradient id="paint6_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint7_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D196"/>
<stop offset="1" stop-color="#00B381"/>
</linearGradient>
<radialGradient id="paint8_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -58,6 +58,8 @@ declare module 'vue' {
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.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']
@@ -90,13 +92,11 @@ declare module 'vue' {
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']
@@ -143,7 +143,6 @@ declare module 'vue' {
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -153,10 +152,9 @@ declare module 'vue' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['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']
@@ -189,7 +187,6 @@ declare module 'vue' {
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']

View File

@@ -20,6 +20,12 @@
<AppInterceptor />
</template>
</tippy>
<HoppButtonSecondary
v-if="platform.platformFeatureFlags.cookiesEnabled ?? false"
:label="t('app.cookies')"
:icon="IconCookie"
@click="showCookiesModal = true"
/>
</div>
<div class="flex">
<tippy
@@ -195,12 +201,17 @@
:show="showDeveloperOptions"
@hide-modal="showDeveloperOptions = false"
/>
<CookiesAllModal
:show="showCookiesModal"
@hide-modal="showCookiesModal = false"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { version } from "~/../package.json"
import IconCookie from "~icons/lucide/cookie"
import IconSidebar from "~icons/lucide/sidebar"
import IconZap from "~icons/lucide/zap"
import IconShare2 from "~icons/lucide/share-2"
@@ -223,7 +234,9 @@ import { invokeAction } from "@helpers/actions"
import { HoppSmartItem } from "@hoppscotch/ui"
const t = useI18n()
const showDeveloperOptions = ref(false)
const showCookiesModal = ref(false)
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
const SIDEBAR = useSetting("SIDEBAR")

View File

@@ -1,7 +1,9 @@
<template>
<div>
<header
ref="headerRef"
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
>
<div
class="inline-flex items-center justify-start flex-1 space-x-2"

View File

@@ -258,7 +258,7 @@ const importFromJSON = () => {
inputChooseFileToImportFrom.value.value = ""
}
const exportJSON = () => {
const exportJSON = async () => {
const dataToWrite = collectionJson.value
const parsedCollections = JSON.parse(dataToWrite)
@@ -268,24 +268,32 @@ const exportJSON = () => {
}
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
platform?.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "gql",
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "Hoppscotch Collection JSON file",
extensions: ["json"],
},
],
})
// TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
if (result.type === "unknown" || result.type === "saved") {
platform?.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "gql",
})
toast.success(t("state.download_started").toString())
}
}
</script>

View File

@@ -18,13 +18,12 @@
"
>
<WorkspaceCurrent :section="t('tab.collections')" />
<HoppSmartInput
<input
v-model="filterTexts"
:placeholder="t('action.search')"
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
type="search"
:autofocus="false"
autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8"
:placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'"
/>
</div>
@@ -1867,28 +1866,25 @@ const getJSONCollection = async () => {
* @param collectionJSON - JSON string of the collection
* @param name - Name of the collection set as the file name
*/
const initializeDownloadCollection = (
const initializeDownloadCollection = async (
collectionJSON: string,
name: string | null
) => {
const file = new Blob([collectionJSON], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
const result = await platform.io.saveFileWithDialog({
data: collectionJSON,
contentType: "application/json",
suggestedFilename: `${name ?? "collection"}.json`,
filters: [
{
name: "Hoppscotch Collection JSON file",
extensions: ["json"],
},
],
})
if (name) {
a.download = `${name}.json`
} else {
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
if (result.type === "unknown" || result.type === "saved") {
toast.success(t("state.download_started").toString())
}
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
/**
@@ -1917,11 +1913,14 @@ const exportData = async (
exportLoading.value = false
return
},
(coll) => {
async (coll) => {
const hoppColl = teamCollToHoppRESTColl(coll)
const collectionJSONString = JSON.stringify(hoppColl)
initializeDownloadCollection(collectionJSONString, hoppColl.name)
await initializeDownloadCollection(
collectionJSONString,
hoppColl.name
)
exportLoading.value = false
}
)

View File

@@ -0,0 +1,269 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('app.cookies')"
aria-modal="true"
@close="hideModal"
>
<template #body>
<HoppSmartPlaceholder
v-if="!currentInterceptorSupportsCookies"
:text="t('cookies.modal.interceptor_no_support')"
>
<AppInterceptor class="p-2 border rounded border-dividerLight" />
</HoppSmartPlaceholder>
<div v-else class="flex flex-col">
<div
class="flex bg-primary space-x-2 border-b sticky border-dividerLight -mx-4 px-4 py-4 -mt-4"
style="top: calc(-1 * var(--line-height-body))"
>
<HoppSmartInput
v-model="newDomainText"
class="flex-1"
:placeholder="t('cookies.modal.new_domain_name')"
@keyup.enter="addNewDomain"
/>
<HoppButtonSecondary
outline
filled
:label="t('action.add')"
@click="addNewDomain"
/>
</div>
<div class="flex flex-col space-y-4">
<HoppSmartPlaceholder
v-if="workingCookieJar.size === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('cookies.modal.empty_domains')}`"
:text="t('cookies.modal.empty_domains')"
class="mt-6"
>
</HoppSmartPlaceholder>
<div
v-for="[domain, entries] in workingCookieJar.entries()"
v-else
:key="domain"
class="flex flex-col"
>
<div class="flex items-center justify-between flex-1">
<label for="cookiesList" class="p-4">
{{ domain }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
:icon="IconTrash2"
@click="deleteDomain(domain)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
@click="addCookieToDomain(domain)"
/>
</div>
</div>
<div class="border rounded border-divider">
<div class="divide-y divide-dividerLight">
<div
v-if="entries.length === 0"
class="flex flex-col gap-2 p-4 items-center"
>
{{ t("cookies.modal.no_cookies_in_domain") }}
</div>
<template v-else>
<div
v-for="(entry, entryIndex) in entries"
:key="`${entry}-${entryIndex}`"
class="flex divide-x divide-dividerLight"
>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
:value="entry"
readonly
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.edit')"
:icon="IconEdit"
@click="editCookie(domain, entryIndex, entry)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteCookie(domain, entryIndex)"
/>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="currentInterceptorSupportsCookies" #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
v-focus
:label="t('action.save')"
outline
@click="saveCookieChanges"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="cancelCookieChanges"
/>
</span>
<HoppButtonSecondary
:label="t('action.clear_all')"
outline
filled
@click="clearAllDomains"
/>
</template>
</HoppSmartModal>
<CookiesEditCookie
:show="!!showEditModalFor"
:entry="showEditModalFor"
@save-cookie="saveCookie"
@hide-modal="showEditModalFor = null"
/>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useService } from "dioc/vue"
import { CookieJarService } from "~/services/cookie-jar.service"
import IconTrash from "~icons/lucide/trash"
import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2"
import IconPlus from "~icons/lucide/plus"
import { cloneDeep } from "lodash-es"
import { ref, watch, computed } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
import { EditCookieConfig } from "./EditCookie.vue"
import { useColorMode } from "@composables/theming"
import { useToast } from "@composables/toast"
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const t = useI18n()
const colorMode = useColorMode()
const toast = useToast()
const newDomainText = ref("")
const interceptorService = useService(InterceptorService)
const cookieJarService = useService(CookieJarService)
const workingCookieJar = ref(cloneDeep(cookieJarService.cookieJar.value))
const currentInterceptorSupportsCookies = computed(() => {
const currentInterceptor = interceptorService.currentInterceptor.value
if (!currentInterceptor) return true
return currentInterceptor.supportsCookies ?? false
})
function addNewDomain() {
if (newDomainText.value === "" || /^\s+$/.test(newDomainText.value)) {
toast.error(`${t("cookies.modal.empty_domain")}`)
return
}
workingCookieJar.value.set(newDomainText.value, [])
newDomainText.value = ""
}
function deleteDomain(domain: string) {
workingCookieJar.value.delete(domain)
}
function addCookieToDomain(domain: string) {
showEditModalFor.value = { type: "create", domain }
}
function clearAllDomains() {
workingCookieJar.value = new Map()
toast.success(`${t("state.cleared")}`)
}
watch(
() => props.show,
(show) => {
if (show) {
workingCookieJar.value = cloneDeep(cookieJarService.cookieJar.value)
}
}
)
const showEditModalFor = ref<EditCookieConfig | null>(null)
function saveCookieChanges() {
cookieJarService.cookieJar.value = workingCookieJar.value
hideModal()
}
function cancelCookieChanges() {
hideModal()
}
function editCookie(domain: string, entryIndex: number, cookieEntry: string) {
showEditModalFor.value = {
type: "edit",
domain,
entryIndex,
currentCookieEntry: cookieEntry,
}
}
function deleteCookie(domain: string, entryIndex: number) {
const entry = workingCookieJar.value.get(domain)
if (entry) {
entry.splice(entryIndex, 1)
}
}
function saveCookie(cookie: string) {
if (showEditModalFor.value?.type === "create") {
const { domain } = showEditModalFor.value
const entry = workingCookieJar.value.get(domain)!
entry.push(cookie)
showEditModalFor.value = null
return
}
if (showEditModalFor.value?.type !== "edit") return
const { domain, entryIndex } = showEditModalFor.value!
const entry = workingCookieJar.value.get(domain)
if (entry) {
entry[entryIndex] = cookie
}
showEditModalFor.value = null
}
const hideModal = () => {
emit("hide-modal")
}
</script>

View File

@@ -0,0 +1,195 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('cookies.modal.set')"
@close="hideModal"
>
<template #body>
<div class="border rounded border-dividerLight">
<div class="flex flex-col">
<div class="flex items-center justify-between pl-4">
<label class="font-semibold truncate text-secondaryLight">
{{ t("cookies.modal.cookie_string") }}
</label>
<div class="flex items-center">
<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', allowHTML: true }"
:title="t('action.download_file')"
:icon="downloadIcon"
@click="downloadResponse"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="t('action.copy')"
:icon="copyIcon"
@click="copyResponse"
/>
</div>
</div>
<div class="h-46">
<div
ref="cookieEditor"
class="h-full border-t rounded-b border-dividerLight"
></div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex space-x-2">
<HoppButtonPrimary
v-focus
:label="t('action.save')"
outline
@click="saveCookieChange"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="cancelCookieChange"
/>
</div>
<span class="flex">
<HoppButtonSecondary
:icon="pasteIcon"
:label="`${t('action.paste')}`"
filled
outline
@click="handlePaste"
/>
</span>
</template>
</HoppSmartModal>
</template>
<script lang="ts">
export type EditCookieConfig =
| { type: "create"; domain: string }
| {
type: "edit"
domain: string
entryIndex: number
currentCookieEntry: string
}
</script>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useCodemirror } from "~/composables/codemirror"
import { watch, ref, reactive } from "vue"
import { refAutoReset } from "@vueuse/core"
import IconWrapText from "~icons/lucide/wrap-text"
import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check"
import IconTrash2 from "~icons/lucide/trash-2"
import { useToast } from "~/composables/toast"
import {
useCopyResponse,
useDownloadResponse,
} from "~/composables/lens-actions"
// TODO: Build Managed Mode!
const props = defineProps<{
show: boolean
entry: EditCookieConfig | null
}>()
const emit = defineEmits<{
(e: "save-cookie", cookie: string): void
(e: "hide-modal"): void
}>()
const t = useI18n()
const toast = useToast()
const cookieEditor = ref<HTMLElement>()
const rawCookieString = ref("")
const linewrapEnabled = ref(true)
useCodemirror(
cookieEditor,
rawCookieString,
reactive({
extendedEditorConfig: {
mode: "text/plain",
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
const pasteIcon = refAutoReset<typeof IconClipboard | typeof IconCheck>(
IconClipboard,
1000
)
watch(
() => props.entry,
() => {
if (!props.entry) return
if (props.entry.type === "create") {
rawCookieString.value = ""
return
}
rawCookieString.value = props.entry.currentCookieEntry
}
)
function hideModal() {
emit("hide-modal")
}
function cancelCookieChange() {
hideModal()
}
async function handlePaste() {
try {
const text = await navigator.clipboard.readText()
if (text) {
rawCookieString.value = text
pasteIcon.value = IconCheck
}
} catch (e) {
console.error("Failed to copy: ", e)
toast.error(t("profile.no_permission").toString())
}
}
function saveCookieChange() {
emit("save-cookie", rawCookieString.value)
}
const { copyIcon, copyResponse } = useCopyResponse(rawCookieString)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"",
rawCookieString
)
function clearContent() {
rawCookieString.value = ""
}
</script>

View File

@@ -375,7 +375,7 @@ const importFromPostman = ({
importFromHoppscotch(environments)
}
const exportJSON = () => {
const exportJSON = async () => {
const dataToWrite = environmentJson.value
const parsedCollections = JSON.parse(dataToWrite)
@@ -385,19 +385,27 @@ const exportJSON = () => {
}
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
toast.success(t("state.download_started").toString())
}
}
const getErrorMessage = (err: GQLError<string>) => {

View File

@@ -66,7 +66,7 @@
/>
<HoppSmartTabs
v-model="selectedEnvTab"
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary ${
!isTeamSelected || workspace.type === 'personal'
? 'bg-primaryLight'
: ''

View File

@@ -1,50 +1,55 @@
<template>
<div>
<div class="field-title" :class="{ 'field-highlighted': isHighlighted }">
{{ fieldName }}
<span v-if="fieldArgs.length > 0">
(
<span v-for="(field, index) in fieldArgs" :key="`field-${index}`">
{{ field.name }}:
<GraphqlTypeLink
:gql-type="field.type"
:jump-type-callback="jumpTypeCallback"
/>
<span v-if="index !== fieldArgs.length - 1">, </span>
<div class="flex justify-between gap-2">
<div
class="field-title flex-1"
:class="{ 'field-highlighted': isHighlighted }"
>
{{ fieldName }}
<span v-if="fieldArgs.length > 0">
(
<span v-for="(field, index) in fieldArgs" :key="`field-${index}`">
{{ field.name }}:
<GraphqlTypeLink
:gql-type="field.type"
@jump-to-type="jumpToType"
/>
<span v-if="index !== fieldArgs.length - 1">, </span>
</span>
) </span
>:
<GraphqlTypeLink :gql-type="gqlField.type" @jump-to-type="jumpToType" />
</div>
<div v-if="gqlField.deprecationReason">
<span
v-tippy="{ theme: 'tomato' }"
class="!text-red-500 hover:!text-red-600 text-xs flex items-center gap-2 cursor-pointer"
:title="gqlField.deprecationReason"
>
<IconAlertTriangle /> {{ t("state.deprecated") }}
</span>
) </span
>:
<GraphqlTypeLink
:gql-type="gqlField.type"
:jump-type-callback="jumpTypeCallback"
/>
</div>
</div>
<div
v-if="gqlField.description"
class="py-2 text-secondaryLight field-desc"
class="field-desc py-2 text-secondaryLight"
>
{{ gqlField.description }}
</div>
<div
v-if="gqlField.isDeprecated"
class="inline-block px-2 py-1 my-1 text-black bg-yellow-200 rounded field-deprecated"
>
{{ t("state.deprecated") }}
</div>
<div v-if="fieldArgs.length > 0">
<h5 class="my-2">Arguments:</h5>
<div class="pl-4 border-l-2 border-divider">
<div class="border-l-2 border-divider pl-4">
<div v-for="(field, index) in fieldArgs" :key="`field-${index}`">
<span>
{{ field.name }}:
<GraphqlTypeLink
:gql-type="field.type"
:jump-type-callback="jumpTypeCallback"
@jump-to-type="jumpToType"
/>
</span>
<div
v-if="field.description"
class="py-2 text-secondaryLight field-desc"
class="field-desc py-2 text-secondaryLight"
>
{{ field.description }}
</div>
@@ -54,37 +59,41 @@
</div>
</template>
<script>
// TypeScript + Script Setup this :)
import { defineComponent } from "vue"
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { GraphQLType } from "graphql"
import { computed } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
export default defineComponent({
props: {
gqlField: { type: Object, default: () => ({}) },
jumpTypeCallback: { type: Function, default: () => ({}) },
isHighlighted: { type: Boolean, default: false },
},
setup() {
return {
t: useI18n(),
}
},
computed: {
fieldName() {
return this.gqlField.name
},
const t = useI18n()
fieldArgs() {
return this.gqlField.args || []
},
},
})
const props = withDefaults(
defineProps<{
gqlField: any
isHighlighted: boolean
}>(),
{
gqlField: {},
isHighlighted: false,
}
)
const emit = defineEmits<{
(e: "jump-to-type", type: GraphQLType): void
}>()
const fieldName = computed(() => props.gqlField.name)
const fieldArgs = computed(() => props.gqlField.args || [])
const jumpToType = (type: GraphQLType) => {
emit("jump-to-type", type)
}
</script>
<style lang="scss" scoped>
.field-highlighted {
@apply border-accent border-b-2;
@apply border-b-2 border-accent;
}
.field-title {

View File

@@ -59,6 +59,7 @@ import { useToast } from "@composables/toast"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { GQLResponseEvent } from "~/helpers/graphql/connection"
import { platform } from "~/platform"
const t = useI18n()
const toast = useToast()
@@ -111,21 +112,31 @@ const copyResponse = (str: string) => {
toast.success(`${t("state.copied_to_clipboard")}`)
}
const downloadResponse = (str: string) => {
const downloadResponse = async (str: string) => {
const dataToWrite = str
const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadResponseIcon.value = IconCheck
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
downloadResponseIcon.value = IconCheck
toast.success(`${t("state.download_started")}`)
}
}
defineActionHandler(

View File

@@ -30,8 +30,8 @@
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8"
:placeholder="`${t('action.search')}`"
class="flex flex-1 p-4 py-2 bg-transparent"
/>
<div class="flex">
<HoppButtonSecondary
@@ -58,8 +58,8 @@
v-for="(field, index) in filteredQueryFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
@jump-to-type="handleJumpToType"
/>
</HoppSmartTab>
<HoppSmartTab
@@ -72,8 +72,8 @@
v-for="(field, index) in filteredMutationFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
@jump-to-type="handleJumpToType"
/>
</HoppSmartTab>
<HoppSmartTab
@@ -86,8 +86,8 @@
v-for="(field, index) in filteredSubscriptionFields"
:key="`field-${index}`"
:gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4"
@jump-to-type="handleJumpToType"
/>
</HoppSmartTab>
<HoppSmartTab
@@ -103,7 +103,7 @@
:gql-types="graphqlTypes"
:is-highlighted="isGqlTypeHighlighted(type)"
:highlighted-fields="getGqlTypeHighlightedFields(type)"
:jump-type-callback="handleJumpToType"
@jump-to-type="handleJumpToType"
/>
</HoppSmartTab>
</HoppSmartTabs>
@@ -202,6 +202,7 @@ import {
schemaString,
subscriptionFields,
} from "~/helpers/graphql/connection"
import { platform } from "~/platform"
type NavigationTabs = "history" | "collection" | "docs" | "schema"
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
@@ -372,21 +373,33 @@ useCodemirror(
})
)
const downloadSchema = () => {
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
const downloadSchema = async () => {
const dataToWrite = schemaString.value
const file = new Blob([dataToWrite], { type: "application/graphql" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.graphql`
document.body.appendChild(a)
a.click()
downloadSchemaIcon.value = IconCheck
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
const filename = `${
url.split("/").pop()!.split("#")[0].split("?")[0]
}.graphql`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/graphql",
suggestedFilename: filename,
filters: [
{
name: "GraphQL Schema File",
extensions: ["graphql"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
downloadSchemaIcon.value = IconCheck
toast.success(`${t("state.download_started")}`)
}
}
const copySchema = () => {

View File

@@ -7,38 +7,31 @@
</span>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import { GraphQLScalarType } from "graphql"
<script setup lang="ts">
import { GraphQLScalarType, GraphQLType } from "graphql"
import { computed } from "vue"
export default defineComponent({
props: {
// eslint-disable-next-line vue/require-default-prop
gqlType: null,
// (typeName: string) => void
// eslint-disable-next-line vue/require-default-prop
jumpTypeCallback: Function,
},
const props = defineProps<{
gqlType: GraphQLType
}>()
computed: {
typeString() {
return `${this.gqlType}`
},
isScalar() {
return this.resolveRootType(this.gqlType) instanceof GraphQLScalarType
},
},
const emit = defineEmits<{
(e: "jump-to-type", type: GraphQLType): void
}>()
methods: {
jumpToType() {
if (this.isScalar) return
this.jumpTypeCallback(this.gqlType)
},
resolveRootType(type) {
let t = type
while (t.ofType != null) t = t.ofType
return t
},
},
const typeString = computed(() => `${props.gqlType}`)
const isScalar = computed(() => {
return resolveRootType(props.gqlType) instanceof GraphQLScalarType
})
function resolveRootType(type: GraphQLType) {
let t = type as any
while (t.ofType != null) t = t.ofType
return t
}
function jumpToType() {
if (isScalar.value) return
emit("jump-to-type", props.gqlType)
}
</script>

View File

@@ -9,7 +9,7 @@
v-model="filterText"
type="search"
autocomplete="off"
class="flex flex-1 p-4 py-2 bg-transparent"
class="flex w-full p-4 py-2 bg-transparent h-8"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">

View File

@@ -256,7 +256,7 @@ import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import draggable from "vuedraggable-es"
import { RequestOptionTabs } from "./RequestOptions.vue"
import { RESTOptionTabs } from "./RequestOptions.vue"
import { useCodemirror } from "@composables/codemirror"
import { commonHeaders } from "~/helpers/headers"
import { useI18n } from "@composables/i18n"
@@ -295,7 +295,7 @@ const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const props = defineProps<{ modelValue: HoppRESTRequest }>()
const emit = defineEmits<{
(e: "change-tab", value: RequestOptionTabs): void
(e: "change-tab", value: RESTOptionTabs): void
(e: "update:modelValue", value: HoppRESTRequest): void
}>()

View File

@@ -43,6 +43,7 @@ import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { tokenRequest } from "~/helpers/oauth"
import { getCombinedEnvVariables } from "~/helpers/preRequest"
import * as E from "fp-ts/Either"
const t = useI18n()
const toast = useToast()
@@ -98,7 +99,11 @@ const handleAccessTokenRequest = async () => {
clientSecret: parseTemplateString(clientSecret.value, envVars),
scope: parseTemplateString(scope.value, envVars),
}
await tokenRequest(tokenReqParams)
const res = await tokenRequest(tokenReqParams)
if (res && E.isLeft(res)) {
toast.error(res.left)
}
} catch (e) {
toast.error(`${e}`)
}

View File

@@ -350,7 +350,6 @@ const newSendRequest = async () => {
const streamResult = await streamPromise
requestCancelFunc.value = cancel
if (E.isRight(streamResult)) {
subscribeToStream(
streamResult.right,
@@ -365,6 +364,20 @@ const newSendRequest = async () => {
loading.value = false
},
() => {
// TODO: Change this any to a proper type
const result = (streamResult.right as any).value
if (
result.type === "network_fail" &&
result.error?.error === "NO_PW_EXT_HOOK"
) {
const errorResponse: HoppRESTResponse = {
type: "extension_error",
error: result.error.humanMessage.heading,
component: result.error.component,
req: result.req,
}
updateRESTResponse(errorResponse)
}
loading.value = false
}
)

View File

@@ -11,6 +11,12 @@
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<component
:is="response.component"
v-if="response.type === 'extension_error'"
class="flex-1"
/>
<HoppSmartPlaceholder
v-if="response.type === 'network_fail'"
:src="`/images/states/${colorMode.value}/youre_lost.svg`"

View File

@@ -0,0 +1,98 @@
<template>
<HoppSmartPlaceholder
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
:alt="`${t('error.network_fail')}`"
:heading="t('error.network_fail')"
large
>
<div class="my-1 text-secondaryLight flex flex-col items-center">
<span>
{{ t("error.please_install_extension") }}
</span>
<span>
{{ t("error.check_how_to_add_origin") }}
<HoppSmartLink
blank
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
class="text-accent hover:text-accentDark"
>
here
</HoppSmartLink>
</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>
</HoppSmartPlaceholder>
</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"
import { useColorMode } from "~/composables/theming"
const colorMode = useColorMode()
const t = useI18n()
const interceptorService = useService(InterceptorService)
const extensionService = useService(ExtensionInterceptorService)
const hasChromeExtInstalled = extensionService.chromeExtensionInstalled
const hasFirefoxExtInstalled = extensionService.firefoxExtensionInstalled
const extensionEnabled = computed({
get() {
return (
interceptorService.currentInterceptorID.value ===
extensionService.interceptorID
)
},
set(active) {
if (active) {
interceptorService.currentInterceptorID.value =
extensionService.interceptorID
} else {
interceptorService.currentInterceptorID.value =
platform.interceptors.default
}
},
})
</script>

View File

@@ -35,12 +35,12 @@
v-if="
!teamDetails.loading &&
E.isRight(teamDetails.data) &&
teamDetails.data.right.team.teamMembers
teamDetails.data.right.team?.teamMembers
"
class="border rounded border-divider"
>
<HoppSmartPlaceholder
v-if="teamDetails.data.right.team.teamMembers === 0"
v-if="teamDetails.data.right.team.teamMembers.length === 0"
:src="`/images/states/${colorMode.value}/add_group.svg`"
:alt="`${t('empty.members')}`"
:text="t('empty.members')"

View File

@@ -88,7 +88,7 @@
>
<div
v-for="(invitee, index) in pendingInvites.data.right.team
.teamInvitations"
?.teamInvitations"
:key="`invitee-${index}`"
class="flex divide-x divide-dividerLight"
>
@@ -122,7 +122,7 @@
<HoppSmartPlaceholder
v-if="
E.isRight(pendingInvites.data) &&
pendingInvites.data.right.team.teamInvitations.length === 0
pendingInvites.data.right.team?.teamInvitations.length === 0
"
:text="t('empty.pending_invites')"
>

View File

@@ -10,6 +10,7 @@ import { useI18n } from "./i18n"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "@helpers/utils/clipboard"
import { HoppRESTResponse } from "@helpers/types/HoppRESTResponse"
import { platform } from "~/platform"
export function useCopyResponse(responseBodyText: Ref<any>) {
const toast = useToast()
@@ -40,15 +41,14 @@ export function useDownloadResponse(
const toast = useToast()
const t = useI18n()
const downloadResponse = () => {
const downloadResponse = async () => {
const dataToWrite = responseBody.value
const file = new Blob([dataToWrite], { type: contentType })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO: get uri from meta
a.download = pipe(
// Guess extension and filename
const file = new Blob([dataToWrite], { type: contentType })
const url = URL.createObjectURL(file)
const filename = pipe(
url,
S.split("/"),
RNEA.last,
@@ -58,15 +58,24 @@ export function useDownloadResponse(
RNEA.head
)
document.body.appendChild(a)
a.click()
downloadIcon.value = IconCheck
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
URL.revokeObjectURL(url)
console.log(filename)
// TODO: Look at the mime type and determine extension ?
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: contentType,
suggestedFilename: filename,
})
// Assume success if unknown as we cannot determine
if (result.type === "unknown" || result.type === "saved") {
downloadIcon.value = IconCheck
toast.success(`${t("state.download_started")}`)
}
}
return {
downloadIcon,
downloadResponse,

View File

@@ -152,12 +152,14 @@ export function useStreamSubscriber(): {
error?: (e: any) => void,
complete?: () => void
) => {
const sub = stream.subscribe({
let sub: Subscription | null = null
sub = stream.subscribe({
next,
error,
complete: () => {
if (complete) complete()
subs.splice(subs.indexOf(sub), 1)
if (sub) subs.splice(subs.indexOf(sub), 1)
},
})

View File

@@ -6,7 +6,7 @@ import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
import { BehaviorSubject } from "rxjs"
import { HoppRESTDocument } from "./rest/document"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
import { HoppGQLSaveContext } from "./graphql/document"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { computed } from "vue"
@@ -113,7 +113,7 @@ type HoppActionArgsMap = {
request: HoppGQLRequest
}
"request.open-tab": {
tab: RequestOptionTabs | GQLOptionTabs
tab: RESTOptionTabs | GQLOptionTabs
}
"tab.duplicate-tab": {

View File

@@ -1,6 +1,7 @@
import { Environment } from "@hoppscotch/data"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { cloneDeep } from "lodash-es"
import { platform } from "~/platform"
const getEnvironmentJson = (
environmentObj: TeamEnvironment | Environment,
@@ -32,17 +33,24 @@ export const exportAsJSON = (
if (!dataToWrite) return false
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
setTimeout(() => {
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
}, 0)
URL.revokeObjectURL(url)
platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
suggestedFilename: `${
url.split("/").pop()!.split("#")[0].split("?")[0]
}.json`,
filters: [
{
name: "JSON file",
extensions: ["json"],
},
],
})
return true
}

View File

@@ -4,7 +4,10 @@ import {
removeLocalConfig,
} from "~/newstore/localpersistence"
const redirectUri = `${window.location.origin}/`
import * as E from "fp-ts/Either"
import { z } from "zod"
const redirectUri = `${window.location.origin}/oauth`
// GENERAL HELPER FUNCTIONS
@@ -16,7 +19,7 @@ const redirectUri = `${window.location.origin}/`
* @returns {Object}
*/
const sendPostRequest = async (url, params) => {
const sendPostRequest = async (url: string, params: Record<string, string>) => {
const body = Object.keys(params)
.map((key) => `${key}=${params[key]}`)
.join("&")
@@ -30,9 +33,9 @@ const sendPostRequest = async (url, params) => {
try {
const response = await fetch(url, options)
const data = await response.json()
return data
return E.right(data)
} catch (e) {
console.error(e)
return E.left("AUTH_TOKEN_REQUEST_FAILED")
}
}
@@ -43,7 +46,7 @@ const sendPostRequest = async (url, params) => {
* @returns {Object}
*/
const parseQueryString = (searchQuery) => {
const parseQueryString = (searchQuery: string): Record<string, string> => {
if (searchQuery === "") {
return {}
}
@@ -61,7 +64,7 @@ const parseQueryString = (searchQuery) => {
* @returns {Object}
*/
const getTokenConfiguration = async (endpoint) => {
const getTokenConfiguration = async (endpoint: string) => {
const options = {
method: "GET",
headers: {
@@ -71,9 +74,9 @@ const getTokenConfiguration = async (endpoint) => {
try {
const response = await fetch(endpoint, options)
const config = await response.json()
return config
return E.right(config)
} catch (e) {
console.error(e)
return E.left("OIDC_DISCOVERY_FAILED")
}
}
@@ -97,7 +100,7 @@ const generateRandomString = () => {
* @returns {Promise<ArrayBuffer>}
*/
const sha256 = (plain) => {
const sha256 = (plain: string) => {
const encoder = new TextEncoder()
const data = encoder.encode(plain)
return window.crypto.subtle.digest("SHA-256", data)
@@ -111,15 +114,18 @@ const sha256 = (plain) => {
*/
const base64urlencode = (
str // Converts the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
) =>
str: ArrayBuffer // Converts the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
) => {
const hashArray = Array.from(new Uint8Array(str))
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
return btoa(String.fromCharCode.apply(null, hashArray))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "")
}
/**
* Return the base64-urlencoded sha256 hash for the PKCE challenge
@@ -128,13 +134,23 @@ const base64urlencode = (
* @returns {String}
*/
const pkceChallengeFromVerifier = async (v) => {
const pkceChallengeFromVerifier = async (v: string) => {
const hashed = await sha256(v)
return base64urlencode(hashed)
}
// OAUTH REQUEST
type TokenRequestParams = {
oidcDiscoveryUrl: string
grantType: string
authUrl: string
accessTokenUrl: string
clientId: string
clientSecret: string
scope: string
}
/**
* Initiates PKCE Auth Code flow when requested
*
@@ -150,16 +166,28 @@ const tokenRequest = async ({
clientId,
clientSecret,
scope,
}) => {
}: TokenRequestParams) => {
// Check oauth configuration
if (oidcDiscoveryUrl !== "") {
// eslint-disable-next-line camelcase
const { authorization_endpoint, token_endpoint } =
await getTokenConfiguration(oidcDiscoveryUrl)
// eslint-disable-next-line camelcase
authUrl = authorization_endpoint
// eslint-disable-next-line camelcase
accessTokenUrl = token_endpoint
const res = await getTokenConfiguration(oidcDiscoveryUrl)
const OIDCConfigurationSchema = z.object({
authorization_endpoint: z.string(),
token_endpoint: z.string(),
})
if (E.isLeft(res)) {
return E.left("OIDC_DISCOVERY_FAILED" as const)
}
const parsedOIDCConfiguration = OIDCConfigurationSchema.safeParse(res.right)
if (!parsedOIDCConfiguration.success) {
return E.left("OIDC_DISCOVERY_FAILED" as const)
}
authUrl = parsedOIDCConfiguration.data.authorization_endpoint
accessTokenUrl = parsedOIDCConfiguration.data.token_endpoint
}
// Store oauth information
setLocalConfig("tokenEndpoint", accessTokenUrl)
@@ -190,7 +218,7 @@ const tokenRequest = async ({
)}&code_challenge_method=S256`
// Redirect to the authorization server
window.location = buildUrl()
window.location.assign(buildUrl())
}
// OAUTH REDIRECT HANDLING
@@ -202,44 +230,84 @@ const tokenRequest = async ({
* @returns {Promise<any | void>}
*/
const oauthRedirect = () => {
let tokenResponse = ""
const q = parseQueryString(window.location.search.substring(1))
const handleOAuthRedirect = async () => {
const queryParams = parseQueryString(window.location.search.substring(1))
// Check if the server returned an error string
if (q.error) {
alert(`Error returned from authorization server: ${q.error}`)
if (queryParams.error) {
return E.left("AUTH_SERVER_RETURNED_ERROR" as const)
}
if (!queryParams.code) {
return E.left("NO_AUTH_CODE" as const)
}
// If the server returned an authorization code, attempt to exchange it for an access token
if (q.code) {
// Verify state matches what we set at the beginning
if (getLocalConfig("pkce_state") !== q.state) {
alert("Invalid state")
Promise.reject(tokenResponse)
} else {
try {
// Exchange the authorization code for an access token
tokenResponse = sendPostRequest(getLocalConfig("tokenEndpoint"), {
grant_type: "authorization_code",
code: q.code,
client_id: getLocalConfig("client_id"),
client_secret: getLocalConfig("client_secret"),
redirect_uri: redirectUri,
code_verifier: getLocalConfig("pkce_codeVerifier"),
})
} catch (e) {
console.error(e)
return Promise.reject(tokenResponse)
}
}
// Clean these up since we don't need them anymore
removeLocalConfig("pkce_state")
removeLocalConfig("pkce_codeVerifier")
removeLocalConfig("tokenEndpoint")
removeLocalConfig("client_id")
removeLocalConfig("client_secret")
return tokenResponse
// Verify state matches what we set at the beginning
if (getLocalConfig("pkce_state") !== queryParams.state) {
return E.left("INVALID_STATE" as const)
}
return Promise.reject(tokenResponse)
const tokenEndpoint = getLocalConfig("tokenEndpoint")
const clientID = getLocalConfig("client_id")
const clientSecret = getLocalConfig("client_secret")
const codeVerifier = getLocalConfig("pkce_codeVerifier")
if (!tokenEndpoint) {
return E.left("NO_TOKEN_ENDPOINT" as const)
}
if (!clientID) {
return E.left("NO_CLIENT_ID" as const)
}
if (!clientSecret) {
return E.left("NO_CLIENT_SECRET" as const)
}
if (!codeVerifier) {
return E.left("NO_CODE_VERIFIER" as const)
}
// Exchange the authorization code for an access token
const tokenResponse: E.Either<string, any> = await sendPostRequest(
tokenEndpoint,
{
grant_type: "authorization_code",
code: queryParams.code,
client_id: clientID,
client_secret: clientSecret,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}
)
// Clean these up since we don't need them anymore
clearPKCEState()
if (E.isLeft(tokenResponse)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const withAccessTokenSchema = z.object({
access_token: z.string(),
})
const parsedTokenResponse = withAccessTokenSchema.safeParse(
tokenResponse.right
)
return parsedTokenResponse.success
? E.right(parsedTokenResponse.data)
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
}
export { tokenRequest, oauthRedirect }
const clearPKCEState = () => {
removeLocalConfig("pkce_state")
removeLocalConfig("pkce_codeVerifier")
removeLocalConfig("tokenEndpoint")
removeLocalConfig("client_id")
removeLocalConfig("client_secret")
}
export { tokenRequest, handleOAuthRedirect }

View File

@@ -1,4 +1,5 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { Component } from "vue"
export type HoppRESTResponseHeader = { key: string; value: string }
@@ -39,3 +40,9 @@ export type HoppRESTResponse =
req: HoppRESTRequest
}
| {
type: "extension_error"
error: string
component: Component
req: HoppRESTRequest
}

View File

@@ -73,6 +73,7 @@ import { applySetting } from "~/newstore/settings"
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
import { useToast } from "~/composables/toast"
import { useI18n } from "~/composables/i18n"
import { platform } from "~/platform"
const router = useRouter()
@@ -98,7 +99,10 @@ onBeforeMount(() => {
onMounted(() => {
const cookiesAllowed = getLocalConfig("cookiesAllowed") === "yes"
if (!cookiesAllowed) {
const platformAllowsCookiePrompts =
platform.platformFeatureFlags.promptAsUsingCookies ?? true
if (!cookiesAllowed && platformAllowsCookiePrompts) {
toast.show(`${t("app.we_use_cookies")}`, {
duration: 0,
action: [

View File

@@ -58,7 +58,13 @@ export const FALLBACK_LANG = pipe(
)
// A reference to the i18n instance
let i18nInstance: I18n<any, any, any> | null = null
let i18nInstance: I18n<
Record<string, unknown>,
Record<string, unknown>,
Record<string, unknown>,
string,
true
> | null = null
const resolveCurrentLocale = () =>
pipe(
@@ -119,7 +125,6 @@ export const changeAppLanguage = async (locale: string) => {
* Returns the i18n instance
*/
export function getI18n() {
// @ts-expect-error Something weird with the i18n errors
return i18nInstance!.global.t
}

View File

@@ -47,6 +47,9 @@ import { StorageLike, watchDebounced } from "@vueuse/core"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
import { z } from "zod"
import { CookieJarService } from "~/services/cookie-jar.service"
import { watch } from "vue"
function checkAndMigrateOldSettings() {
if (window.localStorage.getItem("selectedEnvIndex")) {
@@ -182,6 +185,35 @@ function setupHistoryPersistence() {
})
}
const cookieSchema = z.record(z.array(z.string()))
function setupCookiesPersistence() {
const cookieJarService = getService(CookieJarService)
try {
const cookieData = JSON.parse(
window.localStorage.getItem("cookieJar") || "{}"
)
const parseResult = cookieSchema.safeParse(cookieData)
if (parseResult.success) {
for (const domain in parseResult.data) {
cookieJarService.bulkApplyCookiesToDomain(
parseResult.data[domain],
domain
)
}
}
} catch (e) {}
watch(cookieJarService.cookieJar, (cookieJar) => {
const data = JSON.stringify(Object.fromEntries(cookieJar.entries()))
window.localStorage.setItem("cookieJar", data)
})
}
function setupCollectionsPersistence() {
const restCollectionData = JSON.parse(
window.localStorage.getItem("collections") || "[]"
@@ -382,6 +414,8 @@ export function setupLocalPersistence() {
setupSocketIOPersistence()
setupSSEPersistence()
setupMQTTPersistence()
setupCookiesPersistence()
}
/**

View File

@@ -94,7 +94,7 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
import { ref, onMounted, onBeforeUnmount } from "vue"
import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { useRoute } from "vue-router"
@@ -114,7 +114,6 @@ import {
} from "rxjs"
import { useToast } from "~/composables/toast"
import { watchDebounced } from "@vueuse/core"
import { oauthRedirect } from "~/helpers/oauth"
import { useReadonlyStream } from "~/composables/stream"
import {
changeCurrentSyncStatus,
@@ -414,28 +413,6 @@ function setupTabStateSync() {
})
}
function oAuthURL() {
onBeforeMount(async () => {
try {
const tokenInfo = await oauthRedirect()
if (
typeof tokenInfo === "object" &&
tokenInfo.hasOwnProperty("access_token")
) {
if (
tabs.currentActiveTab.value.document.request.auth.authType ===
"oauth-2"
) {
tabs.currentActiveTab.value.document.request.auth.token =
tokenInfo.access_token
}
}
// eslint-disable-next-line no-empty
} catch (_) {}
})
}
defineActionHandler("contextmenu.open", ({ position, text }) => {
if (text) {
contextMenu.value = {
@@ -454,7 +431,6 @@ defineActionHandler("contextmenu.open", ({ position, text }) => {
setupTabStateSync()
bindRequestToURLParams()
oAuthURL()
defineActionHandler("rest.request.open", ({ doc }) => {
tabs.createNewTab(doc)

View File

@@ -0,0 +1,43 @@
<template>
<div class="flex justify-center items-center">
<HoppSmartSpinner />
</div>
</template>
<script setup lang="ts">
import { handleOAuthRedirect } from "~/helpers/oauth"
import { useToast } from "~/composables/toast"
import * as E from "fp-ts/Either"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { onMounted } from "vue"
import { useRouter } from "vue-router"
const router = useRouter()
const toast = useToast()
const tabs = useService(RESTTabService)
onMounted(async () => {
const tokenInfo = await handleOAuthRedirect()
if (E.isLeft(tokenInfo)) {
toast.error(tokenInfo.left)
router.push("/")
return
}
if (
tabs.currentActiveTab.value.document.request.auth.authType === "oauth-2"
) {
tabs.currentActiveTab.value.document.request.auth.token =
tokenInfo.right.access_token
router.push("/")
return
}
})
</script>

View File

@@ -9,12 +9,14 @@ import { AnalyticsPlatformDef } from "./analytics"
import { InterceptorsPlatformDef } from "./interceptors"
import { HoppModule } from "~/modules"
import { InspectorsPlatformDef } from "./inspectors"
import { IOPlatformDef } from "./io"
export type PlatformDef = {
ui?: UIPlatformDef
addedHoppModules?: HoppModule[]
auth: AuthPlatformDef
analytics?: AnalyticsPlatformDef
io: IOPlatformDef
sync: {
environments: EnvironmentsPlatformDef
collections: CollectionsPlatformDef
@@ -27,6 +29,20 @@ export type PlatformDef = {
platformFeatureFlags: {
exportAsGIST: boolean
hasTelemetry: boolean
/**
* Whether the platform supports cookies (affects whether the cookies footer item is shown)
* If a value is not given, then the value is assumed to be false
*/
cookiesEnabled?: boolean
/**
* Whether the platform should prompt the user that cookies are being used.
* This will result in the user being notified a cookies advisory and is meant for web apps.
*
* If a value is not given, then the value is assumed to be true
*/
promptAsUsingCookies?: boolean
}
}

View File

@@ -0,0 +1,84 @@
/**
* Defines how to save a file to the user's filesystem.
*/
export type SaveFileWithDialogOptions = {
/**
* The data to be saved
*/
data: string | ArrayBuffer
/**
* The suggested filename for the file. This name will be shown in the
* save dialog by default when a save is initiated.
*/
suggestedFilename: string
/**
* The content type mime type of the data to be saved.
*
* NOTE: The usage of this data might be platform dependent.
* For example, this field is used in the web, but not in the desktop app.
*/
contentType: string
/**
* Defines the filters (like in Windows, on the right side, where you can
* select the file type) for the file dialog.
*
* NOTE: The usage of this data might be platform dependent.
* For example, this field is used in the web, but not in the desktop app.
*/
filters?: Array<{
/**
* The name of the filter (in Windows, if the filter looks
* like "Images (*.png, *.jpg)", the name would be "Images")
*/
name: string
/**
* The array of extensions that are supported, without the dot.
*/
extensions: string[]
}>
}
export type SaveFileResponse =
| {
/**
* The implementation was unable to determine the status of the save operation.
* This cannot be considered a success or a failure and should be handled as an uncertainity.
* The browser standard implementation (std) returns this value as there is no way to
* check if the user downloaded the file or not.
*/
type: "unknown"
}
| {
/**
* The result is known and the user cancelled the save.
*/
type: "cancelled"
}
| {
/**
* The result is known and the user saved the file.
*/
type: "saved"
/**
* The full path of where the file was saved
*/
path: string
}
/**
* Platform definitions for how to handle IO operations.
*/
export type IOPlatformDef = {
/**
* Defines how to save a file to the user's filesystem.
* The expected behaviour is for the browser to show a prompt to save the file.
*/
saveFileWithDialog: (
opts: SaveFileWithDialogOptions
) => Promise<SaveFileResponse>
}

View File

@@ -12,6 +12,7 @@ import { computed, readonly, ref } from "vue"
import { browserIsChrome, browserIsFirefox } from "~/helpers/utils/userAgent"
import SettingsExtension from "~/components/settings/Extension.vue"
import InterceptorsExtensionSubtitle from "~/components/interceptors/ExtensionSubtitle.vue"
import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue"
export const defineSubscribableObject = <T extends object>(obj: T) => {
const proxyObject = {
@@ -217,6 +218,7 @@ export class ExtensionInterceptorService
description: () => "Heading not found",
},
error: "NO_PW_EXT_HOOK",
component: InterceptorsErrorPlaceholder,
})
}

View File

@@ -0,0 +1,37 @@
import { IOPlatformDef } from "../io"
import { pipe } from "fp-ts/function"
import * as S from "fp-ts/string"
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
/**
* Implementation for how to handle IO operations in the browser.
*/
export const browserIODef: IOPlatformDef = {
saveFileWithDialog(opts) {
const file = new Blob([opts.data], { type: opts.contentType })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = pipe(
url,
S.split("/"),
RNEA.last,
S.split("#"),
RNEA.head,
S.split("?"),
RNEA.head
)
document.body.appendChild(a)
a.click()
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
// Browsers provide no way for us to know the save went successfully.
return Promise.resolve({ type: "unknown" })
},
}

View File

@@ -20,6 +20,11 @@ export type UIPlatformDef = {
appHeader?: {
paddingTop?: Ref<string>
paddingLeft?: Ref<string>
/**
* A function which is called when the header area of the app receives a click event
*/
onHeaderAreaClick?: () => void
}
onCodemirrorInstanceMount?: (element: HTMLElement) => void

View File

@@ -72,6 +72,61 @@ describe("InterceptorService", () => {
expect(service.currentInterceptorID.value).not.toEqual("unknown")
})
it("currentInterceptor points to the instance of the currently selected interceptor", () => {
const container = new TestContainer()
const service = container.bind(InterceptorService)
const interceptor = {
interceptorID: "test",
name: () => "test interceptor",
selectable: { type: "selectable" as const },
runRequest: () => {
throw new Error("not implemented")
},
}
service.registerInterceptor(interceptor)
service.currentInterceptorID.value = "test"
expect(service.currentInterceptor.value).toBe(interceptor)
})
it("currentInterceptor updates when the currentInterceptorID changes", () => {
const container = new TestContainer()
const service = container.bind(InterceptorService)
const interceptor = {
interceptorID: "test",
name: () => "test interceptor",
selectable: { type: "selectable" as const },
runRequest: () => {
throw new Error("not implemented")
},
}
const interceptor_2 = {
interceptorID: "test2",
name: () => "test interceptor",
selectable: { type: "selectable" as const },
runRequest: () => {
throw new Error("not implemented")
},
}
service.registerInterceptor(interceptor)
service.registerInterceptor(interceptor_2)
service.currentInterceptorID.value = "test"
expect(service.currentInterceptor.value).toBe(interceptor)
service.currentInterceptorID.value = "test2"
expect(service.currentInterceptor.value).not.toBe(interceptor)
expect(service.currentInterceptor.value).toBe(interceptor_2)
})
describe("registerInterceptor", () => {
it("should register the interceptor", () => {
const container = new TestContainer()

View File

@@ -0,0 +1,69 @@
import { Service } from "dioc"
import { ref } from "vue"
import { parseString as setCookieParse } from "set-cookie-parser-es"
export type CookieDef = {
name: string
value: string
domain: string
path: string
expires: string
}
export class CookieJarService extends Service {
public static readonly ID = "COOKIE_JAR_SERVICE"
/**
* The cookie jar that stores all relevant cookie info.
* The keys correspond to the domain of the cookie.
* The cookie strings are stored as an array of strings corresponding to the domain
*/
public cookieJar = ref(new Map<string, string[]>())
constructor() {
super()
}
public parseSetCookieString(setCookieString: string) {
return setCookieParse(setCookieString)
}
public bulkApplyCookiesToDomain(cookies: string[], domain: string) {
const existingDomainEntries = this.cookieJar.value.get(domain) ?? []
existingDomainEntries.push(...cookies)
this.cookieJar.value.set(domain, existingDomainEntries)
}
public getCookiesForURL(url: URL) {
const relevantDomains = Array.from(this.cookieJar.value.keys()).filter(
(domain) => url.hostname.endsWith(domain)
)
return relevantDomains
.flatMap((domain) => {
// Assemble the list of cookie entries from all the relevant domains
const cookieStrings = this.cookieJar.value.get(domain)! // We know not nullable from how we filter above
return cookieStrings.map((cookieString) =>
this.parseSetCookieString(cookieString)
)
})
.filter((cookie) => {
// Perform the required checks on the cookies
const passesPathCheck = url.pathname.startsWith(cookie.path ?? "/")
const passesExpiresCheck = !cookie.expires
? true
: cookie.expires.getTime() >= new Date().getTime()
const passesSecureCheck = !cookie.secure
? true
: url.protocol === "https:"
return passesPathCheck && passesExpiresCheck && passesSecureCheck
})
}
}

View File

@@ -4,6 +4,7 @@ import { HeaderInspectorService } from "../header.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
@@ -58,5 +59,48 @@ describe("HeaderInspectorService", () => {
expect(result.value).toHaveLength(0)
})
it("should return an empty array when headers contain cookies but interceptor supports cookies", () => {
const container = new TestContainer()
container.bindMock(InterceptorService, {
currentInterceptor: ref({ supportsCookies: true }) as any,
})
const headerInspector = container.bind(HeaderInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
})
const result = headerInspector.getInspections(req)
expect(result.value).toHaveLength(0)
})
it("should return an inspector result when headers contain cookies and the current interceptor doesn't support cookies", () => {
const container = new TestContainer()
container.bindMock(InterceptorService, {
currentInterceptor: ref({ supportsCookies: false }) as any,
})
const headerInspector = container.bind(HeaderInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
})
const result = headerInspector.getInspections(req)
expect(result.value).not.toHaveLength(0)
expect(result.value).toContainEqual(
expect.objectContaining({ id: "header", isApplicable: true })
)
})
})
})

View File

@@ -4,6 +4,7 @@ import { getI18n } from "~/modules/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { Ref, computed, markRaw } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import { InterceptorService } from "~/services/interceptor.service"
/**
* This inspector is responsible for inspecting the header of a request.
@@ -19,6 +20,7 @@ export class HeaderInspectorService extends Service implements Inspector {
public readonly inspectorID = "header"
private readonly inspection = this.bind(InspectionService)
private readonly interceptorService = this.bind(InterceptorService)
constructor() {
super()
@@ -42,7 +44,10 @@ export class HeaderInspectorService extends Service implements Inspector {
const isContainCookies = headerKeys.includes("Cookie")
if (isContainCookies) {
if (
isContainCookies &&
!this.interceptorService.currentInterceptor.value?.supportsCookies
) {
headerKeys.forEach((headerKey, index) => {
if (this.cookiesCheck(headerKey)) {
results.push({

View File

@@ -29,6 +29,7 @@ export type InterceptorError =
description: (t: ReturnType<typeof getI18n>) => string
}
error?: unknown
component?: Component
}
/**
@@ -85,6 +86,12 @@ export type Interceptor<Err extends InterceptorError = InterceptorError> = {
*/
name: (t: ReturnType<typeof getI18n>) => MaybeRef<string>
/**
* Defines whether the interceptor has support for cookies.
* If this field is undefined, it is assumed as not supporting cookies.
*/
supportsCookies?: boolean
/**
* Defines what to render in the Interceptor section of the Settings page.
* Use this space to define interceptor specific settings.
@@ -161,6 +168,16 @@ export class InterceptorService extends Service {
Array.from(this.interceptors.values())
)
/**
* Gives an instance to the current interceptor.
* NOTE: Do not update from here, this is only for reading.
*/
public currentInterceptor = computed(() => {
if (this.currentInterceptorID.value === null) return null
return this.interceptors.get(this.currentInterceptorID.value)
})
constructor() {
super()

View File

@@ -0,0 +1,92 @@
import fs from "fs"
import { glob } from "glob"
import path from "path"
import ts from "typescript"
import vueTsc from "vue-tsc"
import { fileURLToPath } from "url"
/**
* Helper function to find files to perform type check on
*/
const findFilesToPerformTypeCheck = (directoryPaths, filePatterns) => {
const files = []
directoryPaths.forEach((directoryPath) => {
if (!fs.existsSync(directoryPath)) {
console.error(`Directory not found: ${directoryPath}`)
process.exit(1)
}
files.push(
...glob.sync(filePatterns, {
cwd: directoryPath,
ignore: ["**/__tests__/**", "**/*.d.ts"],
absolute: true,
})
)
})
return files
}
// Derive the current file's directory path `__dirname` from the URL of this module `__filename`
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Define the directory paths and file patterns to perform type checks on
const directoryPaths = [path.resolve(__dirname, "src", "services")]
const filePatterns = ["**/*.ts"]
const tsConfigFileName = path.resolve(__dirname, "tsconfig.json")
const tsConfig = ts.readConfigFile(tsConfigFileName, ts.sys.readFile)
const { options } = ts.parseJsonConfigFileContent(
tsConfig.config,
ts.sys,
__dirname
)
const files = findFilesToPerformTypeCheck(directoryPaths, filePatterns)
const host = ts.createCompilerHost(options)
const program = vueTsc.createProgram({
rootNames: files,
options: { ...options, noEmit: true },
host,
})
// Perform type checking
const diagnostics = ts
.getPreEmitDiagnostics(program)
// Filter diagnostics to include only errors from files in the specified directory
.filter(({ file }) => {
if (!file) {
return false
}
return directoryPaths.some((directoryPath) =>
path.resolve(file.fileName).includes(directoryPath)
)
})
if (!diagnostics.length) {
console.log("Type checking passed.")
// Success
process.exit(0)
}
console.log("TypeScript diagnostics:")
const formatHost = {
getCanonicalFileName: (fileName) => fileName,
getCurrentDirectory: host.getCurrentDirectory,
getNewLine: () => ts.sys.newLine,
}
const formattedDiagnostics = ts.formatDiagnosticsWithColorAndContext(
diagnostics,
formatHost
)
console.error(formattedDiagnostics)
// Failure
process.exit(1)

View File

@@ -34,16 +34,16 @@
},
"homepage": "https://github.com/hoppscotch/hoppscotch#readme",
"devDependencies": {
"@types/lodash": "^4.14.181",
"@types/lodash": "^4.14.200",
"typescript": "^5.2.2",
"vite": "^3.2.3"
"vite": "^4.5.0"
},
"dependencies": {
"fp-ts": "^2.11.10",
"io-ts": "^2.2.16",
"fp-ts": "^2.16.1",
"io-ts": "^2.2.20",
"lodash": "^4.17.21",
"parser-ts": "^0.6.16",
"parser-ts": "^0.7.0",
"verzod": "^0.1.1",
"zod": "^3.22.2"
"zod": "^3.22.4"
}
}

View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Sitemap
.sitemap-gen
# Backend Code generation
src/api/generated

View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}

View File

@@ -0,0 +1,16 @@
# Tauri + Vue 3 + TypeScript
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).

View File

@@ -0,0 +1,18 @@
overwrite: true
schema: "../../gql-gen/*.gql"
generates:
src/api/generated/graphql.ts:
documents: "src/**/*.graphql"
plugins:
- add:
content: >
/* eslint-disable */
// Auto-generated file (DO NOT EDIT!!!), refer gql-codegen.yml
- typescript
- typescript-operations
- typed-document-node
- typescript-urql-graphcache
src/api/generated/backend-schema.json:
plugins:
- urql-introspection

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hoppscotch - Open source API development ecosystem</title>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="apple-touch-icon" href="/icon.png" />
</head>
<body>
<div id="app"></div>
<script>
// Shims to make swagger-parser package work
window.global = window
</script>
<script type="module">
import { Buffer } from "buffer"
import process from "process"
// // Shims to make postman-collection work
window.Buffer = Buffer
window.process = process
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,118 @@
import { IHTMLTag } from "vite-plugin-html-config"
export const APP_INFO = {
name: "Hoppscotch",
shortDescription: "Open source API development ecosystem",
description:
"Helps you create requests faster, saving precious time on development.",
keywords:
"hoppscotch, hopp scotch, hoppscotch online, hoppscotch app, postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket, sse, graphql, socketio",
app: {
background: "#202124",
},
social: {
twitter: "@hoppscotch_io",
},
} as const
export const META_TAGS = (env: Record<string, string>): IHTMLTag[] => [
{
name: "keywords",
content: APP_INFO.keywords,
},
{
name: "X-UA-Compatible",
content: "IE=edge, chrome=1",
},
{
name: "name",
content: `${APP_INFO.name}${APP_INFO.shortDescription}`,
},
{
name: "description",
content: APP_INFO.description,
},
{
name: "image",
content: `${env.VITE_BASE_URL}/banner.png`,
},
// Open Graph tags
{
name: "og:title",
content: `${APP_INFO.name}${APP_INFO.shortDescription}`,
},
{
name: "og:description",
content: APP_INFO.description,
},
{
name: "og:image",
content: `${env.VITE_BASE_URL}/banner.png`,
},
// Twitter tags
{
name: "twitter:card",
content: "summary_large_image",
},
{
name: "twitter:site",
content: APP_INFO.social.twitter,
},
{
name: "twitter:creator",
content: APP_INFO.social.twitter,
},
{
name: "twitter:title",
content: `${APP_INFO.name}${APP_INFO.shortDescription}`,
},
{
name: "twitter:description",
content: APP_INFO.description,
},
{
name: "twitter:image",
content: `${env.VITE_BASE_URL}/banner.png`,
},
// Add to homescreen for Chrome on Android. Fallback for PWA (handled by nuxt)
{
name: "application-name",
content: APP_INFO.name,
},
// Windows phone tile icon
{
name: "msapplication-TileImage",
content: `${env.VITE_BASE_URL}/icon.png`,
},
{
name: "msapplication-TileColor",
content: APP_INFO.app.background,
},
{
name: "msapplication-tap-highlight",
content: "no",
},
// iOS Safari
{
name: "apple-mobile-web-app-title",
content: APP_INFO.name,
},
{
name: "apple-mobile-web-app-capable",
content: "yes",
},
{
name: "apple-mobile-web-app-status-bar-style",
content: "black-translucent",
},
// PWA
{
name: "theme-color",
content: APP_INFO.app.background,
},
{
name: "mask-icon",
content: "/icon.png",
color: APP_INFO.app.background,
},
]

View File

@@ -0,0 +1,77 @@
{
"name": "@hoppscotch/selfhost-desktop",
"private": true,
"version": "2023.8.3-1",
"type": "module",
"scripts": {
"dev:vite": "vite",
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\" --watch",
"dev": "pnpm exec npm-run-all -p -l dev:*",
"build": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build",
"preview": "vite preview",
"tauri": "tauri",
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\""
},
"dependencies": {
"@hoppscotch/common": "workspace:^",
"@platform/auth": "^0.1.106",
"@tauri-apps/api": "^1.3.0",
"@tauri-apps/cli": "^1.3.0",
"@vueuse/core": "^10.4.1",
"axios": "^0.21.4",
"buffer": "^6.0.3",
"dioc": "workspace:^",
"environments.api": "link:@platform/environments/environments.api",
"event": "link:@tauri-apps/api/event",
"fp-ts": "^2.16.0",
"lodash-es": "^4.17.21",
"process": "^0.11.10",
"rxjs": "^7.8.1",
"shell": "link:@tauri-apps/api/shell",
"stream-browserify": "^3.0.0",
"tauri": "link:@tauri-apps/api/tauri",
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1",
"util": "^0.12.4",
"vue": "^3.2.45",
"workbox-window": "^6.5.4"
},
"devDependencies": {
"@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",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@rushstack/eslint-patch": "^1.1.4",
"@types/lodash-es": "^4.17.9",
"@types/node": "^18.7.10",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"@vitejs/plugin-legacy": "^2.3.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-typescript": "^11.0.1",
"cross-env": "^7.0.3",
"eslint": "^8.28.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.5.1",
"npm-run-all": "^4.1.5",
"typescript": "^4.9.5",
"unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.21.0",
"vite": "^4.2.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.0",
"vite-plugin-pwa": "^0.13.1",
"vite-plugin-static-copy": "^0.12.0",
"vite-plugin-vue-layouts": "^0.7.0",
"vite-plugin-windicss": "^1.8.8",
"vue-tsc": "^1.0.11",
"windicss": "^3.5.6"
}
}

View File

@@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
[package]
name = "hoppscotch-desktop"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.4.0", features = [] }
[dependencies]
tauri = { version = "1.4.1", features = [
"dialog-save",
"fs-write-file",
"http-all",
"os-all",
"shell-open",
"window-start-dragging",
"http-multipart",
] }
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-deep-link = { git = "https://github.com/FabianLars/tauri-plugin-deep-link", branch = "main" }
tauri-plugin-window-state = "0.1.0"
reqwest = "0.11.20"
serde_json = "1.0.107"
url = "2.4.1"
hex_color = "2.0.0"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0"
objc = "0.2.7"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.51.1", features = [
"Win32_Graphics_Dwm",
"Win32_Foundation",
"Win32_UI_Controls",
] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<!-- Obviously needs to be replaced with your app's bundle identifier -->
<string>io.hoppscotch.desktop</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- register the myapp:// and myscheme:// schemes -->
<string>hoppscotch</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -0,0 +1 @@
pub mod window;

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