Compare commits

...

25 Commits

Author SHA1 Message Date
Andrew Bastin
faab1d20fd chore: bump version to 2023.12.6 2024-02-26 22:31:58 +05:30
Anwarul Islam
bd406616ec fix: collection level authorization inheritance issue (#3852) 2024-02-23 19:39:55 +05:30
Andrew Bastin
6827e97ec5 refactor: possible links in email templates do not highlight (#3851) 2024-02-23 01:05:20 +05:30
amk-dev
10d2048975 fix: use x-www-form-urlencoded for token exchange requests 2024-02-22 00:43:50 +05:30
Nivedin
291f18591e fix: perfomance in safari (#3848) 2024-02-22 00:41:30 +05:30
James George
342532c9b1 fix(common): prevent exceptions with open shared requests in new tab action (#3835) 2024-02-22 00:36:45 +05:30
James George
4bd54b12cd fix(persistence-service): add fallbacks for environments related schemas (#3832) 2024-02-15 23:38:56 +05:30
Andrew Bastin
ed6e9b6954 chore: bump version to 2023.12.5 2024-02-15 21:47:58 +05:30
James George
dfdd44b4ed fix(persistence-service): update global environment variables schema (#3829) 2024-02-15 21:40:31 +05:30
Akash K
fc34871dae fix: accessing undefined property variables (#3831) 2024-02-15 21:32:50 +05:30
Nivedin
45b532747e fix: environment tooltip update bug (#3819) 2024-02-13 17:42:02 +05:30
Akash K
de4635df23 chore: add workspace type property in request run analytics event (#3820)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2024-02-13 17:38:11 +05:30
Nivedin
41bad1f3dc fix: secret environment flow bugs (#3817) 2024-02-10 20:22:10 +05:30
Andrew Bastin
ecca3d2032 chore: correct linting errors 2024-02-09 14:42:12 +05:30
Muhammed Ajmal M
47226be6d0 feat: persist line wrap setting (#3647)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-09 14:05:09 +05:30
Andrew Bastin
6a0e73fdec chore: bump versions 2024-02-08 22:41:59 +05:30
Andrew Bastin
672ee69b2c chore: correct linting errors 2024-02-08 22:33:03 +05:30
James George
c0fae79678 fix(sh-admin): persist active selection in the sidebar (#3812) 2024-02-08 22:16:33 +05:30
James George
5bcc38e36b feat: support secret environment variables in CLI (#3815) 2024-02-08 22:08:18 +05:30
Nivedin
00862eb192 feat: secret variables in environments (#3779)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-08 21:58:42 +05:30
Akash K
16803acb26 chore: Oauth temporary ux improvements (#3792) 2024-02-06 20:35:29 +05:30
Nivedin
3911c9cd1f refactor: update share request flow (#3805) 2024-02-05 23:50:15 +05:30
Florian Metz
0028f6e878 feat(js-sandbox): expose atob & btoa functions for Node.js (#3724)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-05 23:12:55 +05:30
Anwarul Islam
0ba33ec187 fix: request endpoint heading (#3804) 2024-02-05 23:08:16 +05:30
James George
d7cdeb796a chore(common): analytics on spotlight (#3727)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
2024-02-02 15:32:06 +05:30
144 changed files with 4003 additions and 990 deletions

View File

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

View File

@@ -25,7 +25,7 @@ export class MailerService {
): string { ): string {
switch (mailDesc.template) { switch (mailDesc.template) {
case 'team-invitation': case 'team-invitation':
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`; return `A user has invited you to join a team workspace in Hoppscotch`;
case 'user-invitation': case 'user-invitation':
return 'Sign in to Hoppscotch'; return 'Sign in to Hoppscotch';

View File

@@ -27,6 +27,12 @@
color: #3869D4; color: #3869D4;
} }
a.nohighlight {
color: inherit !important;
text-decoration: none !important;
cursor: default !important;
}
a img { a img {
border: none; border: none;
} }
@@ -458,7 +464,7 @@
<td class="content-cell"> <td class="content-cell">
<div class="f-fallback"> <div class="f-fallback">
<h1>Hi there,</h1> <h1>Hi there,</h1>
<p>{{invitee}} with {{invite_team_name}} has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p> <p><a class="nohighlight" name="invitee" href="#">{{invitee}}</a> with <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a> has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
<!-- Action --> <!-- Action -->
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0"> <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr> <tr>
@@ -484,7 +490,7 @@
Welcome aboard, <br /> Welcome aboard, <br />
Your friends at Hoppscotch Your friends at Hoppscotch
</p> </p>
<p><strong>P.S.</strong> If you don't associate with {{invitee}} or {{invite_team_name}}, just ignore this email.</p> <p><strong>P.S.</strong> If you don't associate with <a class="nohighlight" name="invitee" href="#">{{invitee}}</a> or <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a>, just ignore this email.</p>
<!-- Sub copy --> <!-- Sub copy -->
<table class="body-sub"> <table class="body-sub">
<tr> <tr>

View File

@@ -14,7 +14,7 @@
--> -->
<style type="text/css" rel="stylesheet" media="all"> <style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */ /* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap"); @import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body { body {
width: 100% !important; width: 100% !important;
@@ -22,19 +22,25 @@
margin: 0; margin: 0;
-webkit-text-size-adjust: none; -webkit-text-size-adjust: none;
} }
a { a {
color: #3869D4; color: #3869D4;
} }
a.nohighlight {
color: inherit !important;
text-decoration: none !important;
cursor: default !important;
}
a img { a img {
border: none; border: none;
} }
td { td {
word-break: break-word; word-break: break-word;
} }
.preheader { .preheader {
display: none !important; display: none !important;
visibility: hidden; visibility: hidden;
@@ -47,13 +53,13 @@
overflow: hidden; overflow: hidden;
} }
/* Type ------------------------------ */ /* Type ------------------------------ */
body, body,
td, td,
th { th {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
} }
h1 { h1 {
margin-top: 0; margin-top: 0;
color: #333333; color: #333333;
@@ -61,7 +67,7 @@
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
} }
h2 { h2 {
margin-top: 0; margin-top: 0;
color: #333333; color: #333333;
@@ -69,7 +75,7 @@
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
} }
h3 { h3 {
margin-top: 0; margin-top: 0;
color: #333333; color: #333333;
@@ -77,12 +83,12 @@
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
} }
td, td,
th { th {
font-size: 16px; font-size: 16px;
} }
p, p,
ul, ul,
ol, ol,
@@ -91,25 +97,25 @@
font-size: 16px; font-size: 16px;
line-height: 1.625; line-height: 1.625;
} }
p.sub { p.sub {
font-size: 13px; font-size: 13px;
} }
/* Utilities ------------------------------ */ /* Utilities ------------------------------ */
.align-right { .align-right {
text-align: right; text-align: right;
} }
.align-left { .align-left {
text-align: left; text-align: left;
} }
.align-center { .align-center {
text-align: center; text-align: center;
} }
/* Buttons ------------------------------ */ /* Buttons ------------------------------ */
.button { .button {
background-color: #3869D4; background-color: #3869D4;
border-top: 10px solid #3869D4; border-top: 10px solid #3869D4;
@@ -124,7 +130,7 @@
-webkit-text-size-adjust: none; -webkit-text-size-adjust: none;
box-sizing: border-box; box-sizing: border-box;
} }
.button--green { .button--green {
background-color: #22BC66; background-color: #22BC66;
border-top: 10px solid #22BC66; border-top: 10px solid #22BC66;
@@ -132,7 +138,7 @@
border-bottom: 10px solid #22BC66; border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66; border-left: 18px solid #22BC66;
} }
.button--red { .button--red {
background-color: #FF6136; background-color: #FF6136;
border-top: 10px solid #FF6136; border-top: 10px solid #FF6136;
@@ -140,7 +146,7 @@
border-bottom: 10px solid #FF6136; border-bottom: 10px solid #FF6136;
border-left: 18px solid #FF6136; border-left: 18px solid #FF6136;
} }
@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
.button { .button {
width: 100% !important; width: 100% !important;
@@ -148,21 +154,21 @@
} }
} }
/* Attribute list ------------------------------ */ /* Attribute list ------------------------------ */
.attributes { .attributes {
margin: 0 0 21px; margin: 0 0 21px;
} }
.attributes_content { .attributes_content {
background-color: #F4F4F7; background-color: #F4F4F7;
padding: 16px; padding: 16px;
} }
.attributes_item { .attributes_item {
padding: 0; padding: 0;
} }
/* Related Items ------------------------------ */ /* Related Items ------------------------------ */
.related { .related {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -171,31 +177,31 @@
-premailer-cellpadding: 0; -premailer-cellpadding: 0;
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
} }
.related_item { .related_item {
padding: 10px 0; padding: 10px 0;
color: #CBCCCF; color: #CBCCCF;
font-size: 15px; font-size: 15px;
line-height: 18px; line-height: 18px;
} }
.related_item-title { .related_item-title {
display: block; display: block;
margin: .5em 0 0; margin: .5em 0 0;
} }
.related_item-thumb { .related_item-thumb {
display: block; display: block;
padding-bottom: 10px; padding-bottom: 10px;
} }
.related_heading { .related_heading {
border-top: 1px solid #CBCCCF; border-top: 1px solid #CBCCCF;
text-align: center; text-align: center;
padding: 25px 0 10px; padding: 25px 0 10px;
} }
/* Discount Code ------------------------------ */ /* Discount Code ------------------------------ */
.discount { .discount {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -206,33 +212,33 @@
background-color: #F4F4F7; background-color: #F4F4F7;
border: 2px dashed #CBCCCF; border: 2px dashed #CBCCCF;
} }
.discount_heading { .discount_heading {
text-align: center; text-align: center;
} }
.discount_body { .discount_body {
text-align: center; text-align: center;
font-size: 15px; font-size: 15px;
} }
/* Social Icons ------------------------------ */ /* Social Icons ------------------------------ */
.social { .social {
width: auto; width: auto;
} }
.social td { .social td {
padding: 0; padding: 0;
width: auto; width: auto;
} }
.social_icon { .social_icon {
height: 20px; height: 20px;
margin: 0 8px 10px 8px; margin: 0 8px 10px 8px;
padding: 0; padding: 0;
} }
/* Data table ------------------------------ */ /* Data table ------------------------------ */
.purchase { .purchase {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -241,7 +247,7 @@
-premailer-cellpadding: 0; -premailer-cellpadding: 0;
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
} }
.purchase_content { .purchase_content {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -250,50 +256,50 @@
-premailer-cellpadding: 0; -premailer-cellpadding: 0;
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
} }
.purchase_item { .purchase_item {
padding: 10px 0; padding: 10px 0;
color: #51545E; color: #51545E;
font-size: 15px; font-size: 15px;
line-height: 18px; line-height: 18px;
} }
.purchase_heading { .purchase_heading {
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 1px solid #EAEAEC; border-bottom: 1px solid #EAEAEC;
} }
.purchase_heading p { .purchase_heading p {
margin: 0; margin: 0;
color: #85878E; color: #85878E;
font-size: 12px; font-size: 12px;
} }
.purchase_footer { .purchase_footer {
padding-top: 15px; padding-top: 15px;
border-top: 1px solid #EAEAEC; border-top: 1px solid #EAEAEC;
} }
.purchase_total { .purchase_total {
margin: 0; margin: 0;
text-align: right; text-align: right;
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
} }
.purchase_total--label { .purchase_total--label {
padding: 0 15px 0 0; padding: 0 15px 0 0;
} }
body { body {
background-color: #F2F4F6; background-color: #F2F4F6;
color: #51545E; color: #51545E;
} }
p { p {
color: #51545E; color: #51545E;
} }
.email-wrapper { .email-wrapper {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -303,7 +309,7 @@
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
background-color: #F2F4F6; background-color: #F2F4F6;
} }
.email-content { .email-content {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -313,16 +319,16 @@
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
} }
/* Masthead ----------------------- */ /* Masthead ----------------------- */
.email-masthead { .email-masthead {
padding: 25px 0; padding: 25px 0;
text-align: center; text-align: center;
} }
.email-masthead_logo { .email-masthead_logo {
width: 94px; width: 94px;
} }
.email-masthead_name { .email-masthead_name {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
@@ -331,7 +337,7 @@
text-shadow: 0 1px 0 white; text-shadow: 0 1px 0 white;
} }
/* Body ------------------------------ */ /* Body ------------------------------ */
.email-body { .email-body {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -340,7 +346,7 @@
-premailer-cellpadding: 0; -premailer-cellpadding: 0;
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
} }
.email-body_inner { .email-body_inner {
width: 570px; width: 570px;
margin: 0 auto; margin: 0 auto;
@@ -350,7 +356,7 @@
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
background-color: #FFFFFF; background-color: #FFFFFF;
} }
.email-footer { .email-footer {
width: 570px; width: 570px;
margin: 0 auto; margin: 0 auto;
@@ -360,11 +366,11 @@
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
text-align: center; text-align: center;
} }
.email-footer p { .email-footer p {
color: #A8AAAF; color: #A8AAAF;
} }
.body-action { .body-action {
width: 100%; width: 100%;
margin: 30px auto; margin: 30px auto;
@@ -374,25 +380,25 @@
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
text-align: center; text-align: center;
} }
.body-sub { .body-sub {
margin-top: 25px; margin-top: 25px;
padding-top: 25px; padding-top: 25px;
border-top: 1px solid #EAEAEC; border-top: 1px solid #EAEAEC;
} }
.content-cell { .content-cell {
padding: 45px; padding: 45px;
} }
/*Media Queries ------------------------------ */ /*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.email-body_inner, .email-body_inner,
.email-footer { .email-footer {
width: 100% !important; width: 100% !important;
} }
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body, body,
.email-body, .email-body,

View File

@@ -1,6 +1,6 @@
{ {
"name": "@hoppscotch/cli", "name": "@hoppscotch/cli",
"version": "0.5.2", "version": "0.6.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.", "description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io", "homepage": "https://hoppscotch.io",
"main": "dist/index.js", "main": "dist/index.js",
@@ -60,6 +60,7 @@
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"tsup": "^7.2.0", "tsup": "^7.2.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"verzod": "^0.2.2",
"zod": "^3.22.4" "zod": "^3.22.4"
} }
} }

View File

@@ -3,138 +3,247 @@ import { ExecException } from "child_process";
import { HoppErrorCode } from "../../types/errors"; import { HoppErrorCode } from "../../types/errors";
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils"; import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
describe("Test 'hopp test <file>' command:", () => { describe("Test `hopp test <file>` command:", () => {
test("No collection file path provided.", async () => { describe("Argument parsing", () => {
const args = "test"; test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
const { stderr } = await runCLI(args); const args = "test";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT"); expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
}); });
test("Collection file not found.", async () => { test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
const args = "test notfound.json"; const args = "invalid-arg";
const { stderr } = await runCLI(args); const { stderr } = await runCLI(args);
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND"); expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
}); });
})
test("Collection file is invalid JSON.", async () => { describe("Supplied collection export file validations", () => {
const args = `test ${getTestJsonFilePath( test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
"malformed-collection.json" const args = "test notfound.json";
)}`; const { stderr } = await runCLI(args);
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR"); expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
}); });
test("Malformed collection file.", async () => { test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
const args = `test ${getTestJsonFilePath( const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
"malformed-collection2.json" const { stderr } = await runCLI(args);
)}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION"); expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
}); });
test("Invalid arguement.", async () => { test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
const args = "invalid-arg"; const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
const { stderr } = await runCLI(args); const { stderr } = await runCLI(args);
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT"); expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
}); });
test("Collection file not JSON type.", async () => { test("Errors with the code `INVALID_FILE_TYPE` if the supplied collection export file doesn't end with the `.json` extension", async () => {
const args = `test ${getTestJsonFilePath("notjson.txt")}`; const args = `test ${getTestJsonFilePath("notjson-coll.txt", "collection")}`;
const { stderr } = await runCLI(args); const { stderr } = await runCLI(args);
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE"); expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
}); });
test("Some errors occured (exit code 1).", async () => { test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
const args = `test ${getTestJsonFilePath("fails.json")}`; const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).not.toBeNull(); expect(error).not.toBeNull();
expect(error).toMatchObject(<ExecException>{ expect(error).toMatchObject(<ExecException>{
code: 1, code: 1,
});
}); });
}); });
test("No errors occured (exit code 0).", async () => { test("Successfully processes a supplied collection export file of the expected format", async () => {
const args = `test ${getTestJsonFilePath("passes.json")}`; const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
}); });
test("Supports inheriting headers and authorization set at the root collection", async () => { test("Successfully inherits headers and authorization set at the root collection", async () => {
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`; const args = `test ${getTestJsonFilePath(
"collection-level-headers-auth-coll.json", "collection"
)}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
}) });
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
const args = `test ${getTestJsonFilePath(
"pre-req-script-env-var-persistence-coll.json", "collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
}); });
describe("Test 'hopp test <file> --env <file>' command:", () => { describe("Test `hopp test <file> --env <file>` command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath( describe("Supplied environment export file validations", () => {
"passes.json" const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
)}`;
test("No env file path provided.", async () => { test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
const args = `${VALID_TEST_ARGS} --env`; const args = `${VALID_TEST_ARGS} --env`;
const { stderr } = await runCLI(args); const { stderr } = await runCLI(args);
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT"); expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
"notjson-coll.txt", "collection"
)}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Errors with the code `FILE_NOT_FOUND` if the supplied environment export file doesn't exist", async () => {
const args = `${VALID_TEST_ARGS} --env notfound.json`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
const ENV_PATH = getTestJsonFilePath("malformed-envs.json", "environment");
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_ENV_FILE");
});
test("Errors with the code `BULK_ENV_FILE` on supplying an environment export file based on the bulk environment export format", async () => {
const ENV_PATH = getTestJsonFilePath("bulk-envs.json", "environment");
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("BULK_ENV_FILE");
});
}); });
test("ENV file not JSON type.", async () => { test("Successfully resolves values from the supplied environment export file", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`; const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
const { stderr } = await runCLI(args); const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("ENV file not found.", async () => {
const args = `${VALID_TEST_ARGS} --env notfound.json`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("No errors occured (exit code 0).", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`; const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
}); });
test("Correctly resolves environment variables referenced in the request body", async () => { test("Successfully resolves environment variables referenced in the request body", async () => {
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json"); const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json"); const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`; const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
}); });
test("Works with shorth `-e` flag", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
describe("Secret environment variables", () => {
jest.setTimeout(10000);
// Reads secret environment values from system environment
test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => {
const env = {
...process.env,
secretBearerToken: "test-token",
secretBasicAuthUsername: "test-user",
secretBasicAuthPassword: "test-pass",
secretQueryParamValue: "secret-query-param-value",
secretBodyValue: "secret-body-value",
secretHeaderValue: "secret-header-value",
};
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args, { env });
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
// Prefers values specified in the environment export file over values set in the system environment
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
// Values set from the scripting context takes the highest precedence
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-coll.json", "collection"
);
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-coll.json", "collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-envs.json", "environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
}); });
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => { describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath( const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
"passes.json"
)}`;
test("No value passed to delay flag.", async () => { test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay`; const args = `${VALID_TEST_ARGS} --delay`;
const { stderr } = await runCLI(args); const { stderr } = await runCLI(args);
@@ -142,7 +251,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT"); expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
}); });
test("Invalid value passed to delay flag.", async () => { test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay 'NaN'`; const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
const { stderr } = await runCLI(args); const { stderr } = await runCLI(args);
@@ -150,10 +259,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT"); expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
}); });
test("Valid value passed to delay flag.", async () => { test("Successfully performs delayed request execution for a valid delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay 1`; const args = `${VALID_TEST_ARGS} --delay 1`;
const { error } = await runCLI(args); const { error } = await runCLI(args);
expect(error).toBeNull(); expect(error).toBeNull();
}); });
test("Works with the short `-d` flag", async () => {
const args = `${VALID_TEST_ARGS} -d 1`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
}); });

View File

@@ -0,0 +1,21 @@
{
"v": 2,
"name": "pre-req-script-env-var-persistence-coll",
"folders": [],
"requests": [
{
"v": "1",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "sample-req",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://echo.hoppscotch.io",
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
"preRequestScript": "pw.env.set(\"variable\", \"value\");"
}
],
"auth": { "authType": "inherit", "authActive": true },
"headers": []
}

View File

@@ -0,0 +1,107 @@
{
"v": 2,
"name": "secret-envs-coll",
"folders": [],
"requests": [
{
"v": "1",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-headers",
"method": "GET",
"params": [],
"headers": [
{
"key": "Secret-Header-Key",
"value": "<<secretHeaderValue>>",
"active": true
}
],
"endpoint": "<<baseURL>>/headers",
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"auth": { "authType": "none", "authActive": true },
"body": {
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
"contentType": "application/json"
},
"name": "test-secret-body",
"method": "POST",
"params": [],
"headers": [],
"endpoint": "<<baseURL>>/post",
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
},
{
"v": "1",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-query-params",
"method": "GET",
"params": [
{
"key": "secretQueryParamKey",
"value": "<<secretQueryParamValue>>",
"active": true
}
],
"headers": [],
"endpoint": "<<baseURL>>/get",
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
},
{
"v": "1",
"auth": {
"authType": "basic",
"password": "<<secretBasicAuthPassword>>",
"username": "<<secretBasicAuthUsername>>",
"authActive": true
},
"body": { "body": null, "contentType": null },
"name": "test-secret-basic-auth",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
"preRequestScript": ""
},
{
"v": "1",
"auth": {
"token": "<<secretBearerToken>>",
"authType": "bearer",
"password": "testpassword",
"username": "testuser",
"authActive": true
},
"body": { "body": null, "contentType": null },
"name": "test-secret-bearer-auth",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "<<baseURL>>/bearer",
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
},
{
"v": "1",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-fallback",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "<<baseURL>>",
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
"preRequestScript": ""
}
],
"auth": { "authType": "inherit", "authActive": false },
"headers": []
}

View File

@@ -0,0 +1,143 @@
{
"v": 2,
"name": "secret-envs-setters-coll",
"folders": [],
"requests": [
{
"v": "1",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-headers",
"method": "GET",
"params": [],
"headers": [
{
"key": "Secret-Header-Key",
"value": "<<secretHeaderValue>>",
"active": true
}
],
"endpoint": "<<baseURL>>/headers",
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-headers-overrides",
"method": "GET",
"params": [],
"headers": [
{
"key": "Secret-Header-Key",
"value": "<<secretHeaderValue>>",
"active": true
}
],
"endpoint": "<<baseURL>>/headers",
"testScript": "pw.test(\"Value set at the pre-request script takes precedence\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value-overriden\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value-overriden\")\n})",
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
"contentType": "application/json"
},
"name": "test-secret-body",
"method": "POST",
"params": [],
"headers": [],
"endpoint": "<<baseURL>>/post",
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
},
{
"v": "1",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-query-params",
"method": "GET",
"params": [
{
"key": "secretQueryParamKey",
"value": "<<secretQueryParamValue>>",
"active": true
}
],
"headers": [],
"endpoint": "<<baseURL>>/get",
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
},
{
"v": "1",
"auth": {
"authType": "basic",
"password": "<<secretBasicAuthPassword>>",
"username": "<<secretBasicAuthUsername>>",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-basic-auth",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
},
{
"v": "1",
"auth": {
"token": "<<secretBearerToken>>",
"authType": "bearer",
"password": "testpassword",
"username": "testuser",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-bearer-auth",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "<<baseURL>>/bearer",
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
"preRequestScript": "let secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n\nif (!secretBearerToken) {\n pw.env.set(\"secretBearerToken\", \"test-token\")\n secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n}\n\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
}
],
"auth": {
"authType": "inherit",
"authActive": false
},
"headers": []
}

View File

@@ -0,0 +1,30 @@
{
"v": 2,
"name": "secret-envs-persistence-scripting-req",
"folders": [],
"requests": [
{
"v": "1",
"endpoint": "https://httpbin.org/post",
"name": "req",
"params": [],
"headers": [
{
"active": true,
"key": "Custom-Header",
"value": "<<customHeaderValueFromSecretVar>>"
}
],
"method": "POST",
"auth": { "authType": "none", "authActive": true },
"preRequestScript": "pw.env.set(\"preReqVarOne\", \"pre-req-value-one\")\n\npw.env.set(\"preReqVarTwo\", \"pre-req-value-two\")\n\npw.env.set(\"customHeaderValueFromSecretVar\", \"custom-header-secret-value\")\n\npw.env.set(\"customBodyValue\", \"custom-body-value\")",
"testScript": "pw.test(\"Secret environment value set from the pre-request script takes precedence\", () => {\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(\"pre-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the pre-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request headers that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.json.key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})",
"body": {
"contentType": "application/json",
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
}
}
],
"auth": { "authType": "inherit", "authActive": false },
"headers": []
}

View File

@@ -0,0 +1,32 @@
[
{
"v": 0,
"name": "Env-I",
"variables": [
{
"key": "firstName",
"value": "John"
},
{
"key": "lastName",
"value": "Doe"
}
]
},
{
"v": 1,
"id": "2",
"name": "Env-II",
"variables": [
{
"key": "baseUrl",
"value": "https://echo.hoppscotch.io",
"secret": false
},
{
"key": "secretVar",
"secret": true
}
]
}
]

View File

@@ -0,0 +1,16 @@
{
"id": 123,
"v": "1",
"name": "secret-envs",
"values": [
{
"key": "secretVar",
"secret": true
},
{
"key": "regularVar",
"secret": false,
"value": "regular-variable"
}
]
}

View File

@@ -1,4 +1,5 @@
{ {
"v": 0,
"name": "Response body sample", "name": "Response body sample",
"variables": [ "variables": [
{ {
@@ -34,4 +35,4 @@
"value": "<<salutation>> <<fullName>>" "value": "<<salutation>> <<fullName>>"
} }
] ]
} }

View File

@@ -0,0 +1,27 @@
{
"v": 1,
"id": "2",
"name": "secret-envs-persistence-scripting-envs",
"variables": [
{
"key": "preReqVarOne",
"secret": true
},
{
"key": "preReqVarTwo",
"secret": true
},
{
"key": "postReqVarOne",
"secret": true
},
{
"key": "preReqVarTwo",
"secret": true
},
{
"key": "customHeaderValueFromSecretVar",
"secret": true
}
]
}

View File

@@ -0,0 +1,40 @@
{
"id": "2",
"v": 1,
"name": "secret-envs",
"variables": [
{
"key": "secretBearerToken",
"secret": true
},
{
"key": "secretBasicAuthUsername",
"secret": true
},
{
"key": "secretBasicAuthPassword",
"secret": true
},
{
"key": "secretQueryParamValue",
"secret": true
},
{
"key": "secretBodyValue",
"secret": true
},
{
"key": "secretHeaderValue",
"secret": true
},
{
"key": "nonExistentValueInSystemEnv",
"secret": true
},
{
"key": "baseURL",
"value": "https://httpbin.org",
"secret": false
}
]
}

View File

@@ -0,0 +1,46 @@
{
"v": 1,
"id": "2",
"name": "secret-values-envs",
"variables": [
{
"key": "secretBearerToken",
"value": "test-token",
"secret": true
},
{
"key": "secretBasicAuthUsername",
"value": "test-user",
"secret": true
},
{
"key": "secretBasicAuthPassword",
"value": "test-pass",
"secret": true
},
{
"key": "secretQueryParamValue",
"value": "secret-query-param-value",
"secret": true
},
{
"key": "secretBodyValue",
"value": "secret-body-value",
"secret": true
},
{
"key": "secretHeaderValue",
"value": "secret-header-value",
"secret": true
},
{
"key": "nonExistentValueInSystemEnv",
"secret": true
},
{
"key": "baseURL",
"value": "https://httpbin.org",
"secret": false
}
]
}

View File

@@ -3,13 +3,13 @@ import { resolve } from "path";
import { ExecResponse } from "./types"; import { ExecResponse } from "./types";
export const runCLI = (args: string): Promise<ExecResponse> => export const runCLI = (args: string, options = {}): Promise<ExecResponse> =>
{ {
const CLI_PATH = resolve(__dirname, "../../bin/hopp"); const CLI_PATH = resolve(__dirname, "../../bin/hopp");
const command = `node ${CLI_PATH} ${args}` const command = `node ${CLI_PATH} ${args}`
return new Promise((resolve) => return new Promise((resolve) =>
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr })) exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
); );
} }
@@ -25,7 +25,12 @@ export const getErrorCode = (out: string) => {
return ansiTrimmedStr.split(" ")[0]; return ansiTrimmedStr.split(" ")[0];
}; };
export const getTestJsonFilePath = (file: string) => { export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => {
const filePath = resolve(__dirname, `../../src/__tests__/samples/${file}`); const kindDir = {
collection: "collections",
environment: "environments",
}[kind];
const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`);
return filePath; return filePath;
}; };

View File

@@ -21,6 +21,7 @@ export interface RequestStack {
*/ */
export interface RequestConfig extends AxiosRequestConfig { export interface RequestConfig extends AxiosRequestConfig {
supported: boolean; supported: boolean;
displayUrl?: string
} }
export interface EffectiveHoppRESTRequest extends HoppRESTRequest { export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
@@ -30,6 +31,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
* This contains path, params and environment variables all applied to it * This contains path, params and environment variables all applied to it
*/ */
effectiveFinalURL: string; effectiveFinalURL: string;
effectiveFinalDisplayURL?: string;
effectiveFinalHeaders: { key: string; value: string; active: boolean }[]; effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
effectiveFinalParams: { key: string; value: string; active: boolean }[]; effectiveFinalParams: { key: string; value: string; active: boolean }[];
effectiveFinalBody: FormData | string | null; effectiveFinalBody: FormData | string | null;

View File

@@ -1,34 +1,42 @@
import { Environment } from "@hoppscotch/data";
import { entityReference } from "verzod";
import { z } from "zod";
import { error } from "../../types/errors"; import { error } from "../../types/errors";
import { import {
HoppEnvs,
HoppEnvPair,
HoppEnvKeyPairObject, HoppEnvKeyPairObject,
HoppEnvExportObject, HoppEnvPair,
HoppBulkEnvExportObject, HoppEnvs
} from "../../types/request"; } from "../../types/request";
import { readJsonFile } from "../../utils/mutators"; import { readJsonFile } from "../../utils/mutators";
/** /**
* Parses env json file for given path and validates the parsed env json object. * Parses env json file for given path and validates the parsed env json object
* @param path Path of env.json file to be parsed. * @param path Path of env.json file to be parsed
* @returns For successful parsing we get HoppEnvs object. * @returns For successful parsing we get HoppEnvs object
*/ */
export async function parseEnvsData(path: string) { export async function parseEnvsData(path: string) {
const contents = await readJsonFile(path); const contents = await readJsonFile(path);
const envPairs: Array<HoppEnvPair> = []; const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
const HoppBulkEnvExportObjectResult =
HoppBulkEnvExportObject.safeParse(contents);
// CLI doesnt support bulk environments export. // The legacy key-value pair format that is still supported
// Hence we check for this case and throw an error if it matches the format. const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
// Shape of the single environment export object that is exported from the app
const HoppEnvExportObjectResult = Environment.safeParse(contents);
// Shape of the bulk environment export object that is exported from the app
const HoppBulkEnvExportObjectResult = z.array(entityReference(Environment)).safeParse(contents)
// CLI doesnt support bulk environments export
// Hence we check for this case and throw an error if it matches the format
if (HoppBulkEnvExportObjectResult.success) { if (HoppBulkEnvExportObjectResult.success) {
throw error({ code: "BULK_ENV_FILE", path, data: error }); throw error({ code: "BULK_ENV_FILE", path, data: error });
} }
// Checks if the environment file is of the correct format. // Checks if the environment file is of the correct format
// If it doesnt match either of them, we throw an error. // If it doesnt match either of them, we throw an error
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) { if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
throw error({ code: "MALFORMED_ENV_FILE", path, data: error }); throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
} }
@@ -36,8 +44,8 @@ export async function parseEnvsData(path: string) {
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) { for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
envPairs.push({ key, value }); envPairs.push({ key, value });
} }
} else if (HoppEnvExportObjectResult.success) { } else if (HoppEnvExportObjectResult.type === "ok") {
envPairs.push(...HoppEnvExportObjectResult.data.variables); envPairs.push(...HoppEnvExportObjectResult.value.variables);
} }
return <HoppEnvs>{ global: [], selected: envPairs }; return <HoppEnvs>{ global: [], selected: envPairs };

View File

@@ -1,31 +1,18 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { z } from "zod";
import { TestReport } from "../interfaces/response"; import { TestReport } from "../interfaces/response";
import { HoppCLIError } from "./errors"; import { HoppCLIError } from "./errors";
import { z } from "zod";
export type FormDataEntry = { export type FormDataEntry = {
key: string; key: string;
value: string | Blob; value: string | Blob;
}; };
export type HoppEnvPair = { key: string; value: string }; export type HoppEnvPair = Environment["variables"][number];
export const HoppEnvKeyPairObject = z.record(z.string(), z.string()); export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
// Shape of the single environment export object that is exported from the app.
export const HoppEnvExportObject = z.object({
name: z.string(),
variables: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
});
// Shape of the bulk environment export object that is exported from the app.
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
export type HoppEnvs = { export type HoppEnvs = {
global: HoppEnvPair[]; global: HoppEnvPair[];
selected: HoppEnvPair[]; selected: HoppEnvPair[];

View File

@@ -176,7 +176,7 @@ export const printRequestRunner = {
*/ */
start: (requestConfig: RequestConfig) => { start: (requestConfig: RequestConfig) => {
const METHOD = BG_INFO(` ${requestConfig.method} `); const METHOD = BG_INFO(` ${requestConfig.method} `);
const ENDPOINT = requestConfig.url; const ENDPOINT = requestConfig.displayUrl || requestConfig.url;
process.stdout.write(`${METHOD} ${ENDPOINT}`); process.stdout.write(`${METHOD} ${ENDPOINT}`);
}, },

View File

@@ -36,7 +36,10 @@ import { toFormData } from "./mutators";
export const preRequestScriptRunner = ( export const preRequestScriptRunner = (
request: HoppRESTRequest, request: HoppRESTRequest,
envs: HoppEnvs envs: HoppEnvs
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> => ): TE.TaskEither<
HoppCLIError,
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
> =>
pipe( pipe(
TE.of(request), TE.of(request),
TE.chain(({ preRequestScript }) => TE.chain(({ preRequestScript }) =>
@@ -68,7 +71,10 @@ export const preRequestScriptRunner = (
export function getEffectiveRESTRequest( export function getEffectiveRESTRequest(
request: HoppRESTRequest, request: HoppRESTRequest,
environment: Environment environment: Environment
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> { ): E.Either<
HoppCLIError,
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
> {
const envVariables = environment.variables; const envVariables = environment.variables;
// Parsing final headers with applied ENVs. // Parsing final headers with applied ENVs.
@@ -162,12 +168,30 @@ export function getEffectiveRESTRequest(
} }
const effectiveFinalURL = _effectiveFinalURL.right; const effectiveFinalURL = _effectiveFinalURL.right;
// Secret environment variables referenced in the request endpoint should be masked
let effectiveFinalDisplayURL;
if (envVariables.some(({ secret }) => secret)) {
const _effectiveFinalDisplayURL = parseTemplateStringE(
request.endpoint,
envVariables,
true
);
if (E.isRight(_effectiveFinalDisplayURL)) {
effectiveFinalDisplayURL = _effectiveFinalDisplayURL.right;
}
}
return E.right({ return E.right({
...request, effectiveRequest: {
effectiveFinalURL, ...request,
effectiveFinalHeaders, effectiveFinalURL,
effectiveFinalParams, effectiveFinalDisplayURL,
effectiveFinalBody, effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
},
updatedEnvs: { global: [], selected: envVariables },
}); });
} }

View File

@@ -1,4 +1,4 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import axios, { Method } from "axios"; import axios, { Method } from "axios";
import * as A from "fp-ts/Array"; import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either"; import * as E from "fp-ts/Either";
@@ -29,6 +29,38 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported // !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
/**
* Processes given variable, which includes checking for secret variables
* and getting value from system environment
* @param variable Variable to be processed
* @returns Updated variable with value from system environment
*/
const processVariables = (variable: Environment["variables"][number]) => {
if (variable.secret) {
return {
...variable,
value:
"value" in variable ? variable.value : process.env[variable.key] || "",
}
}
return variable
}
/**
* Processes given envs, which includes processing each variable in global
* and selected envs
* @param envs Global + selected envs used by requests with in collection
* @returns Processed envs with each variable processed
*/
const processEnvs = (envs: HoppEnvs) => {
const processedEnvs = {
global: envs.global.map(processVariables),
selected: envs.selected.map(processVariables),
}
return processedEnvs
}
/** /**
* Transforms given request data to request-config used by request-runner to * Transforms given request data to request-config used by request-runner to
* perform HTTP request. * perform HTTP request.
@@ -38,6 +70,7 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => { export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
const config: RequestConfig = { const config: RequestConfig = {
supported: true, supported: true,
displayUrl: req.effectiveFinalDisplayURL
}; };
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest; const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
const reqParams = finalParams(req); const reqParams = finalParams(req);
@@ -221,9 +254,13 @@ export const processRequest =
effectiveFinalParams: [], effectiveFinalParams: [],
effectiveFinalURL: "", effectiveFinalURL: "",
}; };
let updatedEnvs = <HoppEnvs>{};
// Fetch values for secret environment variables from system environment
const processedEnvs = processEnvs(envs)
// Executing pre-request-script // Executing pre-request-script
const preRequestRes = await preRequestScriptRunner(request, envs)(); const preRequestRes = await preRequestScriptRunner(request, processedEnvs)();
if (E.isLeft(preRequestRes)) { if (E.isLeft(preRequestRes)) {
printPreRequestRunner.fail(); printPreRequestRunner.fail();
@@ -231,8 +268,8 @@ export const processRequest =
report.errors.push(preRequestRes.left); report.errors.push(preRequestRes.left);
report.result = report.result && false; report.result = report.result && false;
} else { } else {
// Updating effective-request // Updating effective-request and consuming updated envs after pre-request script execution
effectiveRequest = preRequestRes.right; ({ effectiveRequest, updatedEnvs } = preRequestRes.right);
} }
// Creating request-config for request-runner. // Creating request-config for request-runner.
@@ -270,7 +307,7 @@ export const processRequest =
const testScriptParams = getTestScriptParams( const testScriptParams = getTestScriptParams(
_requestRunnerRes, _requestRunnerRes,
request, request,
envs updatedEnvs
); );
// Executing test-runner. // Executing test-runner.

View File

@@ -429,6 +429,11 @@ pre.ace_editor {
} }
} }
.splitpanes__pane {
@apply will-change-auto;
transform: translateZ(0);
}
.smart-splitter .splitpanes__splitter { .smart-splitter .splitpanes__splitter {
@apply relative; @apply relative;
@apply before:absolute; @apply before:absolute;

View File

@@ -24,6 +24,7 @@
"go_back": "Go back", "go_back": "Go back",
"go_forward": "Go forward", "go_forward": "Go forward",
"group_by": "Group by", "group_by": "Group by",
"hide_secret": "Hide secret",
"label": "Label", "label": "Label",
"learn_more": "Learn more", "learn_more": "Learn more",
"less": "Less", "less": "Less",
@@ -43,6 +44,7 @@
"search": "Search", "search": "Search",
"send": "Send", "send": "Send",
"share": "Share", "share": "Share",
"show_secret": "Show secret",
"start": "Start", "start": "Start",
"starting": "Starting", "starting": "Starting",
"stop": "Stop", "stop": "Stop",
@@ -238,6 +240,7 @@
"profile": "Login to view your profile", "profile": "Login to view your profile",
"protocols": "Protocols are empty", "protocols": "Protocols are empty",
"schema": "Connect to a GraphQL endpoint to view schema", "schema": "Connect to a GraphQL endpoint to view schema",
"secret_environments": "Secrets are not synced to Hoppscotch",
"shared_requests": "Shared requests are empty", "shared_requests": "Shared requests are empty",
"shared_requests_logout": "Login to view your shared requests or create a new one", "shared_requests_logout": "Login to view your shared requests or create a new one",
"subscription": "Subscriptions are empty", "subscription": "Subscriptions are empty",
@@ -269,6 +272,8 @@
"quick_peek": "Environment Quick Peek", "quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable", "replace_with_variable": "Replace with variable",
"scope": "Scope", "scope": "Scope",
"secrets": "Secrets",
"secret_value": "Secret value",
"select": "Select environment", "select": "Select environment",
"set": "Set environment", "set": "Set environment",
"set_as_environment": "Set as environment", "set_as_environment": "Set as environment",
@@ -277,6 +282,7 @@
"updated": "Environment updated", "updated": "Environment updated",
"value": "Value", "value": "Value",
"variable": "Variable", "variable": "Variable",
"variables":"Variables",
"variable_list": "Variable List" "variable_list": "Variable List"
}, },
"error": { "error": {
@@ -413,6 +419,8 @@
"description": "Inspect possible errors", "description": "Inspect possible errors",
"environment": { "environment": {
"add_environment": "Add to Environment", "add_environment": "Add to Environment",
"add_environment_value": "Add value",
"empty_value": "Environment value is empty for the variable '{variable}' ",
"not_found": "Environment variable “{environment}” not found." "not_found": "Environment variable “{environment}” not found."
}, },
"header": { "header": {
@@ -889,6 +897,7 @@
"query": "Query", "query": "Query",
"schema": "Schema", "schema": "Schema",
"shared_requests": "Shared Requests", "shared_requests": "Shared Requests",
"share_tab_request": "Share tab request",
"socketio": "Socket.IO", "socketio": "Socket.IO",
"sse": "SSE", "sse": "SSE",
"tests": "Tests", "tests": "Tests",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@hoppscotch/common", "name": "@hoppscotch/common",
"private": true, "private": true,
"version": "2023.12.3", "version": "2023.12.6",
"scripts": { "scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run", "test": "vitest --run",

View File

@@ -23,7 +23,7 @@
<div class="col-span-1 flex items-center justify-between space-x-2"> <div class="col-span-1 flex items-center justify-between space-x-2">
<button <button
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary" class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
@click="invokeAction('modals.search.toggle')" @click="invokeAction('modals.search.toggle', undefined, 'mouseclick')"
> >
<span class="inline-flex flex-1 items-center"> <span class="inline-flex flex-1 items-center">
<icon-lucide-search class="svg-icons mr-2" /> <icon-lucide-search class="svg-icons mr-2" />

View File

@@ -3,7 +3,7 @@
v-if="show" v-if="show"
styles="sm:max-w-lg" styles="sm:max-w-lg"
full-width full-width
@close="emit('hide-modal')" @close="closeSpotlightModal"
> >
<template #body> <template #body>
<div class="flex flex-col border-b border-divider transition"> <div class="flex flex-col border-b border-divider transition">
@@ -86,35 +86,36 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from "vue"
import { useService } from "dioc/vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useService } from "dioc/vue"
import { isEqual } from "lodash-es"
import { computed, ref, watch } from "vue"
import { platform } from "~/platform"
import { HoppSpotlightSessionEventData } from "~/platform/analytics"
import { import {
SpotlightService,
SpotlightSearchState, SpotlightSearchState,
SpotlightSearcherResult, SpotlightSearcherResult,
SpotlightService,
} from "~/services/spotlight" } from "~/services/spotlight"
import { isEqual } from "lodash-es"
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher" import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
import { import {
EnvironmentsSpotlightSearcherService, EnvironmentsSpotlightSearcherService,
SwitchEnvSpotlightSearcherService, SwitchEnvSpotlightSearcherService,
} from "~/services/spotlight/searchers/environment.searcher" } from "~/services/spotlight/searchers/environment.searcher"
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
import { import {
SwitchWorkspaceSpotlightSearcherService, SwitchWorkspaceSpotlightSearcherService,
WorkspaceSpotlightSearcherService, WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher" } from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
@@ -290,4 +291,17 @@ function newUseArrowKeysForNavigation() {
return { selectedEntry } return { selectedEntry }
} }
function closeSpotlightModal() {
const analyticsData: HoppSpotlightSessionEventData = {
action: "close",
searcherID: null,
rank: null,
}
// Sets the action indicating `close` and rank as `null` in the state for analytics event logging
spotlightService.setAnalyticsData(analyticsData)
emit("hide-modal")
}
</script> </script>

View File

@@ -614,8 +614,8 @@ const addNewRootCollection = (name: string) => {
requests: [], requests: [],
headers: [], headers: [],
auth: { auth: {
authType: "inherit", authType: "none",
authActive: false, authActive: true,
}, },
}) })
) )

View File

@@ -22,9 +22,9 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'cookie')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }" v-tippy="{ theme: 'tooltip', allowHTML: true }"
@@ -102,6 +102,8 @@ import {
useCopyResponse, useCopyResponse,
useDownloadResponse, useDownloadResponse,
} from "~/composables/lens-actions" } from "~/composables/lens-actions"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
// TODO: Build Managed Mode! // TODO: Build Managed Mode!
@@ -122,7 +124,7 @@ const toast = useToast()
const cookieEditor = ref<HTMLElement>() const cookieEditor = ref<HTMLElement>()
const rawCookieString = ref("") const rawCookieString = ref("")
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "cookie")
useCodemirror( useCodemirror(
cookieEditor, cookieEditor,
@@ -131,7 +133,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "text/plain", mode: "text/plain",
placeholder: `${t("cookies.modal.enter_cookie_string")}`, placeholder: `${t("cookies.modal.enter_cookie_string")}`,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -21,7 +21,7 @@
<label for="value" class="min-w-[2.5rem] font-semibold">{{ <label for="value" class="min-w-[2.5rem] font-semibold">{{
t("environment.value") t("environment.value")
}}</label> }}</label>
<input <SmartEnvInput
v-model="editingValue" v-model="editingValue"
type="text" type="text"
class="input" class="input"
@@ -154,12 +154,14 @@ const addEnvironment = async () => {
addGlobalEnvVariable({ addGlobalEnvVariable({
key: editingName.value, key: editingName.value,
value: editingValue.value, value: editingValue.value,
secret: false,
}) })
toast.success(`${t("environment.updated")}`) toast.success(`${t("environment.updated")}`)
} else if (scope.value.type === "my-environment") { } else if (scope.value.type === "my-environment") {
addEnvironmentVariable(scope.value.index, { addEnvironmentVariable(scope.value.index, {
key: editingName.value, key: editingName.value,
value: editingValue.value, value: editingValue.value,
secret: false,
}) })
toast.success(`${t("environment.updated")}`) toast.success(`${t("environment.updated")}`)
} else { } else {

View File

@@ -9,7 +9,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Environment } from "@hoppscotch/data" import { Environment, NonSecretEnvironment } from "@hoppscotch/data"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { ref } from "vue" import { ref } from "vue"
@@ -340,13 +340,13 @@ const showImportFailedError = () => {
const handleImportToStore = async ( const handleImportToStore = async (
environments: Environment[], environments: Environment[],
globalEnv?: Environment globalEnv?: NonSecretEnvironment
) => { ) => {
// if there's a global env, add them to the store // if there's a global env, add them to the store
if (globalEnv) { if (globalEnv) {
globalEnv.variables.forEach(({ key, value }) => { globalEnv.variables.forEach(({ key, value, secret }) =>
addGlobalEnvVariable({ key, value }) addGlobalEnvVariable({ key, value, secret })
}) )
} }
if (props.environmentType === "MY_ENV") { if (props.environmentType === "MY_ENV") {

View File

@@ -210,7 +210,10 @@
{{ variable.key }} {{ variable.key }}
</span> </span>
<span class="min-w-[9rem] w-full truncate text-secondaryLight"> <span class="min-w-[9rem] w-full truncate text-secondaryLight">
{{ variable.value }} <template v-if="variable.secret"> ******** </template>
<template v-else>
{{ variable.value }}
</template>
</span> </span>
</div> </div>
<div v-if="globalEnvs.length === 0" class="text-secondaryLight"> <div v-if="globalEnvs.length === 0" class="text-secondaryLight">
@@ -265,7 +268,10 @@
{{ variable.key }} {{ variable.key }}
</span> </span>
<span class="min-w-[9rem] w-full truncate text-secondaryLight"> <span class="min-w-[9rem] w-full truncate text-secondaryLight">
{{ variable.value }} <template v-if="variable.secret"> ******** </template>
<template v-else>
{{ variable.value }}
</template>
</span> </span>
</div> </div>
<div <div
@@ -479,15 +485,20 @@ const selectedEnv = computed(() => {
type: "MY_ENV", type: "MY_ENV",
index: props.modelValue.index, index: props.modelValue.index,
name: props.modelValue.environment?.name, name: props.modelValue.environment?.name,
variables: props.modelValue.environment?.variables,
} }
} else if (props.modelValue?.type === "team-environment") { } else if (props.modelValue?.type === "team-environment") {
return { return {
type: "TEAM_ENV", type: "TEAM_ENV",
name: props.modelValue.environment.environment.name, name: props.modelValue.environment.environment.name,
teamEnvID: props.modelValue.environment.id, teamEnvID: props.modelValue.environment.id,
variables: props.modelValue.environment.environment.variables,
} }
} }
return { type: "global", name: "Global" } return {
type: "global",
name: "Global",
}
} }
if (selectedEnvironmentIndex.value.type === "MY_ENV") { if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment = const environment =
@@ -582,9 +593,7 @@ const environmentVariables = computed(() => {
}) })
const editGlobalEnv = () => { const editGlobalEnv = () => {
invokeAction("modals.my.environment.edit", { invokeAction("modals.global.environment.update", {})
envName: "Global",
})
} }
const editEnv = () => { const editEnv = () => {

View File

@@ -24,6 +24,8 @@
:action="action" :action="action"
:editing-environment-index="editingEnvironmentIndex" :editing-environment-index="editingEnvironmentIndex"
:editing-variable-name="editingVariableName" :editing-variable-name="editingVariableName"
:env-vars="envVars"
:is-secret-option-selected="secretOptionSelected"
@hide-modal="displayModalEdit(false)" @hide-modal="displayModalEdit(false)"
/> />
<EnvironmentsAdd <EnvironmentsAdd
@@ -37,7 +39,7 @@
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="showConfirmRemoveEnvModal" :show="showConfirmRemoveEnvModal"
:title="t('confirm.remove_team')" :title="`${t('confirm.remove_environment')}`"
@hide-modal="showConfirmRemoveEnvModal = false" @hide-modal="showConfirmRemoveEnvModal = false"
@resolve="removeSelectedEnvironment()" @resolve="removeSelectedEnvironment()"
/> />
@@ -67,6 +69,7 @@ import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironme
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { Environment } from "@hoppscotch/data"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -88,6 +91,8 @@ const environmentType = ref<EnvironmentsChooseType>({
const globalEnv = useReadonlyStream(globalEnv$, []) const globalEnv = useReadonlyStream(globalEnv$, [])
const globalEnvironment = computed(() => ({ const globalEnvironment = computed(() => ({
v: 1 as const,
id: "Global",
name: "Global", name: "Global",
variables: globalEnv.value, variables: globalEnv.value,
})) }))
@@ -186,6 +191,7 @@ const action = ref<"new" | "edit">("edit")
const editingEnvironmentIndex = ref<"Global" | null>(null) const editingEnvironmentIndex = ref<"Global" | null>(null)
const editingVariableName = ref("") const editingVariableName = ref("")
const editingVariableValue = ref("") const editingVariableValue = ref("")
const secretOptionSelected = ref(false)
const position = ref({ top: 0, left: 0 }) const position = ref({ top: 0, left: 0 })
@@ -203,6 +209,7 @@ const displayModalEdit = (shouldDisplay: boolean) => {
const editEnvironment = (environmentIndex: "Global") => { const editEnvironment = (environmentIndex: "Global") => {
editingEnvironmentIndex.value = environmentIndex editingEnvironmentIndex.value = environmentIndex
action.value = "edit" action.value = "edit"
editingVariableName.value = ""
displayModalEdit(true) displayModalEdit(true)
} }
@@ -232,6 +239,9 @@ const removeSelectedEnvironment = () => {
const resetSelectedData = () => { const resetSelectedData = () => {
editingEnvironmentIndex.value = null editingEnvironmentIndex.value = null
editingVariableName.value = ""
editingVariableValue.value = ""
secretOptionSelected.value = false
} }
defineActionHandler("modals.environment.new", () => { defineActionHandler("modals.environment.new", () => {
@@ -243,11 +253,19 @@ defineActionHandler("modals.environment.delete-selected", () => {
showConfirmRemoveEnvModal.value = true showConfirmRemoveEnvModal.value = true
}) })
const additionalVars = ref<Environment["variables"]>([])
const envVars = () => [...globalEnv.value, ...additionalVars.value]
defineActionHandler( defineActionHandler(
"modals.my.environment.edit", "modals.global.environment.update",
({ envName, variableName }) => { ({ variables, isSecret }) => {
if (variableName) editingVariableName.value = variableName if (variables) {
envName === "Global" && editEnvironment("Global") additionalVars.value = variables
}
secretOptionSelected.value = isSecret ?? false
editEnvironment("Global")
editingVariableName.value = "Global"
} }
) )

View File

@@ -16,76 +16,103 @@
@submit="saveEnvironment" @submit="saveEnvironment"
/> />
<div class="flex flex-1 items-center justify-between"> <div class="my-4 flex flex-col border border-divider rounded">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="clearIcon"
@click="clearContent()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconPlus"
:title="t('add.new')"
@click="addEnvironmentVariable"
/>
</div>
</div>
<div
v-if="evnExpandError"
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
>
{{ t("environment.nested_overflow") }}
</div>
<div class="divide-y divide-dividerLight rounded border border-divider">
<div <div
v-for="({ id, env }, index) in vars" v-if="evnExpandError"
:key="`variable-${id}-${index}`" class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
class="flex divide-x divide-dividerLight"
> >
<input {{ t("environment.nested_overflow") }}
v-model="env.key"
v-focus
class="flex flex-1 bg-transparent px-4 py-2"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
:name="'param' + index"
/>
<SmartEnvInput
v-model="env.value"
:select-text-on-mount="env.key === editingVariableName"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
/>
<div class="flex">
<HoppButtonSecondary
id="variable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="removeEnvironmentVariable(index)"
/>
</div>
</div> </div>
<HoppSmartPlaceholder <HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
v-if="vars.length === 0" <template #actions>
:src="`/images/states/${colorMode.value}/blockchain.svg`" <div class="flex flex-1 items-center justify-between">
:alt="`${t('empty.environments')}`" <HoppButtonSecondary
:text="t('empty.environments')" v-tippy="{ theme: 'tooltip' }"
> to="https://docs.hoppscotch.io/documentation/features/environments"
<template #body> blank
<HoppButtonSecondary :title="t('app.wiki')"
:label="`${t('add.new')}`" :icon="IconHelpCircle"
filled />
@click="addEnvironmentVariable" <HoppButtonSecondary
/> v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="clearIcon"
@click="clearContent()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconPlus"
:title="t('add.new')"
@click="addEnvironmentVariable"
/>
</div>
</template> </template>
</HoppSmartPlaceholder>
<HoppSmartTab
v-for="tab in tabsData"
:id="tab.id"
:key="tab.id"
:label="tab.label"
>
<div
class="divide-y divide-dividerLight rounded border border-divider"
>
<HoppSmartPlaceholder
v-if="tab.variables.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="tab.emptyStateLabel"
:text="tab.emptyStateLabel"
>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addEnvironmentVariable"
/>
</template>
</HoppSmartPlaceholder>
<template v-else>
<div
v-for="({ id, env }, index) in tab.variables"
:key="`variable-${id}-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
v-model="env.key"
v-focus
class="flex flex-1 bg-transparent px-4 py-2"
:placeholder="`${t('count.variable', {
count: index + 1,
})}`"
:name="'param' + index"
/>
<SmartEnvInput
v-model="env.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
:secret="tab.isSecret"
:select-text-on-mount="
env.key ? env.key === editingVariableName : false
"
/>
<div class="flex">
<HoppButtonSecondary
id="variable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="removeEnvironmentVariable(id)"
/>
</div>
</div>
</template>
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div> </div>
</div> </div>
</template> </template>
@@ -112,8 +139,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
import IconDone from "~icons/lucide/check" import IconDone from "~icons/lucide/check"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconTrash from "~icons/lucide/trash" import IconTrash from "~icons/lucide/trash"
import { clone } from "lodash-es" import IconHelpCircle from "~icons/lucide/help-circle"
import { computed, ref, watch } from "vue" import { ComputedRef, computed, ref, watch } from "vue"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import * as A from "fp-ts/Array" import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
@@ -136,12 +163,16 @@ import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { environmentsStore } from "~/newstore/environments" import { environmentsStore } from "~/newstore/environments"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { uniqueId } from "lodash-es"
type EnvironmentVariable = { type EnvironmentVariable = {
id: number id: number
env: { env: {
key: string
value: string value: string
key: string
secret: boolean
} }
} }
@@ -155,6 +186,7 @@ const props = withDefaults(
action: "edit" | "new" action: "edit" | "new"
editingEnvironmentIndex?: number | "Global" | null editingEnvironmentIndex?: number | "Global" | null
editingVariableName?: string | null editingVariableName?: string | null
isSecretOptionSelected?: boolean
envVars?: () => Environment["variables"] envVars?: () => Environment["variables"]
}>(), }>(),
{ {
@@ -162,6 +194,7 @@ const props = withDefaults(
action: "edit", action: "edit",
editingEnvironmentIndex: null, editingEnvironmentIndex: null,
editingVariableName: null, editingVariableName: null,
isSecretOptionSelected: false,
envVars: () => [], envVars: () => [],
} }
) )
@@ -172,11 +205,55 @@ const emit = defineEmits<{
const idTicker = ref(0) const idTicker = ref(0)
const tabsData: ComputedRef<
{
id: string
label: string
emptyStateLabel: string
isSecret: boolean
variables: EnvironmentVariable[]
}[]
> = computed(() => {
return [
{
id: "variables",
label: t("environment.variables"),
emptyStateLabel: t("empty.environments"),
isSecret: false,
variables: nonSecretVars.value,
},
{
id: "secret",
label: t("environment.secrets"),
emptyStateLabel: t("empty.secret_environments"),
isSecret: true,
variables: secretVars.value,
},
]
})
const editingName = ref<string | null>(null) const editingName = ref<string | null>(null)
const editingID = ref<string>("")
const vars = ref<EnvironmentVariable[]>([ const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } }, { id: idTicker.value++, env: { key: "", value: "", secret: false } },
]) ])
const secretEnvironmentService = useService(SecretEnvironmentService)
const secretVars = computed(() =>
pipe(
vars.value,
A.filter((e) => e.env.secret)
)
)
const nonSecretVars = computed(() =>
pipe(
vars.value,
A.filter((e) => !e.env.secret)
)
)
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>( const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
IconTrash2, IconTrash2,
1000 1000
@@ -184,14 +261,23 @@ const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
const globalVars = useReadonlyStream(globalEnv$, []) const globalVars = useReadonlyStream(globalEnv$, [])
type SelectedEnv = "variables" | "secret"
const selectedEnvOption = ref<SelectedEnv>("variables")
const workingEnv = computed(() => { const workingEnv = computed(() => {
if (props.editingEnvironmentIndex === "Global") { if (props.editingEnvironmentIndex === "Global") {
const vars =
props.editingVariableName === "Global"
? props.envVars()
: getGlobalVariables()
return { return {
name: "Global", name: "Global",
variables: getGlobalVariables(), variables: vars,
} as Environment } as Environment
} else if (props.action === "new") { } else if (props.action === "new") {
return { return {
id: uniqueId(),
name: "", name: "",
variables: props.envVars(), variables: props.envVars(),
} }
@@ -214,6 +300,7 @@ const evnExpandError = computed(() => {
return pipe( return pipe(
variables, variables,
A.filter(({ secret }) => !secret),
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables))) A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
) )
}) })
@@ -239,11 +326,29 @@ watch(
(show) => { (show) => {
if (show) { if (show) {
editingName.value = workingEnv.value?.name ?? null editingName.value = workingEnv.value?.name ?? null
selectedEnvOption.value = props.isSecretOptionSelected
? "secret"
: "variables"
if (props.editingEnvironmentIndex !== "Global") {
editingID.value = workingEnv.value?.id ?? uniqueId()
}
vars.value = pipe( vars.value = pipe(
workingEnv.value?.variables ?? [], workingEnv.value?.variables ?? [],
A.map((e) => ({ A.mapWithIndex((index, e) => ({
id: idTicker.value++, id: idTicker.value++,
env: clone(e), env: {
key: e.key,
value: e.secret
? secretEnvironmentService.getSecretEnvironmentVariable(
props.editingEnvironmentIndex === "Global"
? "Global"
: workingEnv.value?.id,
index
)?.value ?? ""
: e.value,
secret: e.secret,
},
})) }))
) )
} }
@@ -251,7 +356,10 @@ watch(
) )
const clearContent = () => { const clearContent = () => {
vars.value = [] vars.value = vars.value.filter((e) =>
selectedEnvOption.value === "secret" ? !e.env.secret : e.env.secret
)
clearIcon.value = IconDone clearIcon.value = IconDone
toast.success(`${t("state.cleared")}`) toast.success(`${t("state.cleared")}`)
} }
@@ -262,12 +370,16 @@ const addEnvironmentVariable = () => {
env: { env: {
key: "", key: "",
value: "", value: "",
secret: selectedEnvOption.value === "secret",
}, },
}) })
} }
const removeEnvironmentVariable = (index: number) => { const removeEnvironmentVariable = (id: number) => {
vars.value.splice(index, 1) const index = vars.value.findIndex((e) => e.id === id)
if (index !== -1) {
vars.value.splice(index, 1)
}
} }
const saveEnvironment = () => { const saveEnvironment = () => {
@@ -276,7 +388,7 @@ const saveEnvironment = () => {
return return
} }
const filterdVariables = pipe( const filteredVariables = pipe(
vars.value, vars.value,
A.filterMap( A.filterMap(
flow( flow(
@@ -286,14 +398,43 @@ const saveEnvironment = () => {
) )
) )
const secretVariables = pipe(
filteredVariables,
A.filterMapWithIndex((i, e) =>
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
)
)
if (editingID.value) {
secretEnvironmentService.addSecretEnvironment(
editingID.value,
secretVariables
)
} else if (props.editingEnvironmentIndex === "Global") {
secretEnvironmentService.addSecretEnvironment("Global", secretVariables)
}
const variables = pipe(
filteredVariables,
A.map((e) =>
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
)
)
const environmentUpdated: Environment = { const environmentUpdated: Environment = {
v: 1,
id: uniqueId(),
name: editingName.value, name: editingName.value,
variables: filterdVariables, variables,
} }
if (props.action === "new") { if (props.action === "new") {
// Creating a new environment // Creating a new environment
createEnvironment(editingName.value, environmentUpdated.variables) createEnvironment(
editingName.value,
environmentUpdated.variables,
editingID.value
)
setSelectedEnvironmentIndex({ setSelectedEnvironmentIndex({
type: "MY_ENV", type: "MY_ENV",
index: envList.value.length - 1, index: envList.value.length - 1,
@@ -332,6 +473,7 @@ const saveEnvironment = () => {
const hideModal = () => { const hideModal = () => {
editingName.value = null editingName.value = null
selectedEnvOption.value = "variables"
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -135,6 +135,8 @@ import { useToast } from "@composables/toast"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { HoppSmartItem } from "@hoppscotch/ui" import { HoppSmartItem } from "@hoppscotch/ui"
import { exportAsJSON } from "~/helpers/import-export/export/environment" import { exportAsJSON } from "~/helpers/import-export/export/environment"
import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -150,6 +152,8 @@ const emit = defineEmits<{
const confirmRemove = ref(false) const confirmRemove = ref(false)
const secretEnvironmentService = useService(SecretEnvironmentService)
const exportEnvironmentAsJSON = () => { const exportEnvironmentAsJSON = () => {
const { environment, environmentIndex } = props const { environment, environmentIndex } = props
exportAsJSON(environment, environmentIndex) exportAsJSON(environment, environmentIndex)
@@ -168,6 +172,7 @@ const removeEnvironment = () => {
if (props.environmentIndex === null) return if (props.environmentIndex === null) return
if (props.environmentIndex !== "Global") { if (props.environmentIndex !== "Global") {
deleteEnvironment(props.environmentIndex, props.environment.id) deleteEnvironment(props.environmentIndex, props.environment.id)
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
} }
toast.success(`${t("state.deleted")}`) toast.success(`${t("state.deleted")}`)
} }

View File

@@ -67,6 +67,7 @@
:action="action" :action="action"
:editing-environment-index="editingEnvironmentIndex" :editing-environment-index="editingEnvironmentIndex"
:editing-variable-name="editingVariableName" :editing-variable-name="editingVariableName"
:is-secret-option-selected="secretOptionSelected"
@hide-modal="displayModalEdit(false)" @hide-modal="displayModalEdit(false)"
/> />
<EnvironmentsImportExport <EnvironmentsImportExport
@@ -99,6 +100,7 @@ const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit") const action = ref<"new" | "edit">("edit")
const editingEnvironmentIndex = ref<number | null>(null) const editingEnvironmentIndex = ref<number | null>(null)
const editingVariableName = ref("") const editingVariableName = ref("")
const secretOptionSelected = ref(false)
const displayModalAdd = (shouldDisplay: boolean) => { const displayModalAdd = (shouldDisplay: boolean) => {
action.value = "new" action.value = "new"
@@ -120,18 +122,23 @@ const editEnvironment = (environmentIndex: number) => {
} }
const resetSelectedData = () => { const resetSelectedData = () => {
editingEnvironmentIndex.value = null editingEnvironmentIndex.value = null
editingVariableName.value = ""
secretOptionSelected.value = false
} }
defineActionHandler( defineActionHandler(
"modals.my.environment.edit", "modals.my.environment.edit",
({ envName, variableName }) => { ({ envName, variableName, isSecret }) => {
if (variableName) editingVariableName.value = variableName if (variableName) editingVariableName.value = variableName
const envIndex: number = environments.value.findIndex( const envIndex: number = environments.value.findIndex(
(environment: Environment) => { (environment: Environment) => {
return environment.name === envName return environment.name === envName
} }
) )
if (envName !== "Global") editEnvironment(envIndex) if (envName !== "Global") {
editEnvironment(envIndex)
secretOptionSelected.value = isSecret ?? false
}
} }
) )
</script> </script>

View File

@@ -16,90 +16,112 @@
@submit="saveEnvironment" @submit="saveEnvironment"
/> />
<div class="flex flex-1 items-center justify-between"> <div class="my-4 flex flex-col border border-divider rounded">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}
</label>
<div v-if="!isViewer" class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="clearIcon"
@click="clearContent()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconPlus"
:title="t('add.new')"
@click="addEnvironmentVariable"
/>
</div>
</div>
<div
v-if="evnExpandError"
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
>
{{ t("environment.nested_overflow") }}
</div>
<div class="divide-y divide-dividerLight rounded border border-divider">
<div <div
v-for="({ id, env }, index) in vars" v-if="evnExpandError"
:key="`variable-${id}-${index}`" class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
class="flex divide-x divide-dividerLight"
> >
<input {{ t("environment.nested_overflow") }}
v-model="env.key"
v-focus
class="flex flex-1 bg-transparent px-4 py-2"
:class="isViewer && 'opacity-25'"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
:name="'param' + index"
:disabled="isViewer"
/>
<SmartEnvInput
v-model="env.value"
:select-text-on-mount="env.key === editingVariableName"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
:readonly="isViewer"
/>
<div v-if="!isViewer" class="flex">
<HoppButtonSecondary
id="variable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="removeEnvironmentVariable(index)"
/>
</div>
</div> </div>
<HoppSmartPlaceholder <HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
v-if="vars.length === 0" <template #actions>
:src="`/images/states/${colorMode.value}/blockchain.svg`" <div class="flex flex-1 items-center justify-between">
:alt="`${t('empty.environments')}`" <HoppButtonSecondary
:text="t('empty.environments')" v-tippy="{ theme: 'tooltip' }"
> to="https://docs.hoppscotch.io/documentation/features/environments"
<template #body> blank
<HoppButtonSecondary :title="t('app.wiki')"
v-if="isViewer" :icon="IconHelpCircle"
disabled />
:label="`${t('add.new')}`" <HoppButtonSecondary
filled v-if="!isViewer"
/> v-tippy="{ theme: 'tooltip' }"
<HoppButtonSecondary :title="t('action.clear_all')"
v-else :icon="clearIcon"
:label="`${t('add.new')}`" @click="clearContent()"
filled />
@click="addEnvironmentVariable" <HoppButtonSecondary
/> v-if="!isViewer"
v-tippy="{ theme: 'tooltip' }"
:icon="IconPlus"
:title="t('add.new')"
@click="addEnvironmentVariable"
/>
</div>
</template> </template>
</HoppSmartPlaceholder>
<HoppSmartTab
v-for="tab in tabsData"
:id="tab.id"
:key="tab.id"
:label="tab.label"
>
<div
class="divide-y divide-dividerLight rounded border border-divider"
>
<HoppSmartPlaceholder
v-if="tab.variables.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="tab.emptyStateLabel"
:text="tab.emptyStateLabel"
>
<template #body>
<HoppButtonSecondary
v-if="!isViewer"
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addEnvironmentVariable"
/>
</template>
</HoppSmartPlaceholder>
<template v-else>
<div
v-for="({ id, env }, index) in tab.variables"
:key="`variable-${id}-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
v-model="env.key"
v-focus
class="flex flex-1 bg-transparent px-4 py-2"
:placeholder="`${t('count.variable', {
count: index + 1,
})}`"
:name="'param' + index"
:disabled="isViewer"
/>
<SmartEnvInput
v-model="env.value"
:select-text-on-mount="
env.key ? env.key === editingVariableName : false
"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
:secret="tab.isSecret"
:readonly="isViewer && !tab.isSecret"
/>
<div v-if="!isViewer" class="flex">
<HoppButtonSecondary
id="variable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="removeEnvironmentVariable(id)"
/>
</div>
</div>
</template>
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div> </div>
</div> </div>
</template> </template>
<template v-if="!isViewer" #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
<HoppButtonPrimary <HoppButtonPrimary
:label="`${t('action.save')}`" :label="`${t('action.save')}`"
@@ -119,7 +141,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from "vue" import { ComputedRef, computed, ref, watch } from "vue"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import * as A from "fp-ts/Array" import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
@@ -141,13 +163,17 @@ import IconTrash from "~icons/lucide/trash"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconDone from "~icons/lucide/check" import IconDone from "~icons/lucide/check"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
type EnvironmentVariable = { type EnvironmentVariable = {
id: number id: number
env: { env: {
key: string key: string
value: string value: string
secret: boolean
} }
} }
@@ -163,6 +189,7 @@ const props = withDefaults(
editingTeamId: string | undefined editingTeamId: string | undefined
editingVariableName?: string | null editingVariableName?: string | null
isViewer?: boolean isViewer?: boolean
isSecretOptionSelected?: boolean
envVars?: () => Environment["variables"] envVars?: () => Environment["variables"]
}>(), }>(),
{ {
@@ -172,6 +199,7 @@ const props = withDefaults(
editingTeamId: "", editingTeamId: "",
editingVariableName: null, editingVariableName: null,
isViewer: false, isViewer: false,
isSecretOptionSelected: false,
envVars: () => [], envVars: () => [],
} }
) )
@@ -182,11 +210,59 @@ const emit = defineEmits<{
const idTicker = ref(0) const idTicker = ref(0)
const tabsData: ComputedRef<
{
id: string
label: string
emptyStateLabel: string
isSecret: boolean
variables: EnvironmentVariable[]
}[]
> = computed(() => {
return [
{
id: "variables",
label: t("environment.variables"),
emptyStateLabel: t("empty.environments"),
isSecret: false,
variables: nonSecretVars.value,
},
{
id: "secret",
label: t("environment.secrets"),
emptyStateLabel: t("empty.secret_environments"),
isSecret: true,
variables: secretVars.value,
},
]
})
const editingName = ref<string | null>(null) const editingName = ref<string | null>(null)
const editingID = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([ const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } }, { id: idTicker.value++, env: { key: "", value: "", secret: false } },
]) ])
const secretEnvironmentService = useService(SecretEnvironmentService)
const secretVars = computed(() =>
pipe(
vars.value,
A.filter((e) => e.env.secret)
)
)
const nonSecretVars = computed(() =>
pipe(
vars.value,
A.filter((e) => !e.env.secret)
)
)
type SelectedEnv = "variables" | "secret"
const selectedEnvOption = ref<SelectedEnv>("variables")
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>( const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
IconTrash2, IconTrash2,
1000 1000
@@ -215,22 +291,34 @@ watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
editingName.value = props.editingEnvironment?.environment.name ?? null
selectedEnvOption.value = props.isSecretOptionSelected
? "secret"
: "variables"
if (props.action === "new") { if (props.action === "new") {
editingName.value = null
vars.value = pipe( vars.value = pipe(
props.envVars() ?? [], props.envVars() ?? [],
A.map((e: { key: string; value: string }) => ({ A.map((e) => ({
id: idTicker.value++, id: idTicker.value++,
env: clone(e), env: clone(e),
})) }))
) )
} else if (props.editingEnvironment !== null) { } else if (props.editingEnvironment !== null) {
editingName.value = props.editingEnvironment.environment.name ?? null editingID.value = props.editingEnvironment.id
vars.value = pipe( vars.value = pipe(
props.editingEnvironment.environment.variables ?? [], props.editingEnvironment.environment.variables ?? [],
A.map((e: { key: string; value: string }) => ({ A.mapWithIndex((index, e) => ({
id: idTicker.value++, id: idTicker.value++,
env: clone(e), env: {
key: e.key,
value: e.secret
? secretEnvironmentService.getSecretEnvironmentVariable(
editingID.value ?? "",
index
)?.value ?? ""
: e.value,
secret: e.secret,
},
})) }))
) )
} }
@@ -250,12 +338,16 @@ const addEnvironmentVariable = () => {
env: { env: {
key: "", key: "",
value: "", value: "",
secret: selectedEnvOption.value === "secret",
}, },
}) })
} }
const removeEnvironmentVariable = (index: number) => { const removeEnvironmentVariable = (id: number) => {
vars.value.splice(index, 1) const index = vars.value.findIndex((e) => e.id === id)
if (index !== -1) {
vars.value.splice(index, 1)
}
} }
const isLoading = ref(false) const isLoading = ref(false)
@@ -278,52 +370,102 @@ const saveEnvironment = async () => {
) )
) )
const secretVariables = pipe(
filterdVariables,
A.filterMapWithIndex((i, e) =>
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
)
)
const variables = pipe(
filterdVariables,
A.map((e) =>
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
)
)
const environmentUpdated: Environment = {
v: 1,
id: editingID.value ?? "",
name: editingName.value,
variables,
}
if (props.action === "new") { if (props.action === "new") {
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_CREATE_ENVIRONMENT", type: "HOPP_CREATE_ENVIRONMENT",
workspaceType: "team", workspaceType: "team",
}) })
await pipe( if (!props.isViewer) {
createTeamEnvironment( await pipe(
JSON.stringify(filterdVariables), createTeamEnvironment(
props.editingTeamId, JSON.stringify(environmentUpdated.variables),
editingName.value props.editingTeamId,
), environmentUpdated.name
TE.match( ),
(err: GQLError<string>) => { TE.match(
console.error(err) (err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`) console.error(err)
}, toast.error(`${getErrorMessage(err)}`)
() => { isLoading.value = false
hideModal() },
toast.success(`${t("environment.created")}`) (res) => {
} const envID = res.createTeamEnvironment.id
) if (envID) {
)() secretEnvironmentService.addSecretEnvironment(
envID,
secretVariables
)
}
hideModal()
toast.success(`${t("environment.created")}`)
isLoading.value = false
}
)
)()
}
} else { } else {
if (!props.editingEnvironment) { if (!props.editingEnvironment) {
console.error("No Environment Found") console.error("No Environment Found")
return return
} }
await pipe( if (editingID.value) {
updateTeamEnvironment( secretEnvironmentService.addSecretEnvironment(
JSON.stringify(filterdVariables), editingID.value,
props.editingEnvironment.id, secretVariables
editingName.value
),
TE.match(
(err: GQLError<string>) => {
console.error(err)
toast.error(`${getErrorMessage(err)}`)
},
() => {
hideModal()
toast.success(`${t("environment.updated")}`)
}
) )
)()
// If the user is a viewer, we don't need to update the environment in BE
// just update the secret environment in the local storage
if (props.isViewer) {
hideModal()
toast.success(`${t("environment.updated")}`)
}
}
if (!props.isViewer) {
await pipe(
updateTeamEnvironment(
JSON.stringify(environmentUpdated.variables),
props.editingEnvironment.id,
environmentUpdated.name
),
TE.match(
(err: GQLError<string>) => {
console.error(err)
toast.error(`${getErrorMessage(err)}`)
isLoading.value = false
},
() => {
hideModal()
toast.success(`${t("environment.updated")}`)
isLoading.value = false
}
)
)()
}
} }
isLoading.value = false isLoading.value = false
@@ -331,6 +473,7 @@ const saveEnvironment = async () => {
const hideModal = () => { const hideModal = () => {
editingName.value = null editingName.value = null
selectedEnvOption.value = "variables"
emit("hide-modal") emit("hide-modal")
} }

View File

@@ -19,7 +19,6 @@
</span> </span>
<span> <span>
<tippy <tippy
v-if="!isViewer"
ref="options" ref="options"
interactive interactive
trigger="click" trigger="click"
@@ -57,6 +56,7 @@
/> />
<HoppSmartItem <HoppSmartItem
v-if="!isViewer"
ref="duplicate" ref="duplicate"
:icon="IconCopy" :icon="IconCopy"
:label="`${t('action.duplicate')}`" :label="`${t('action.duplicate')}`"
@@ -69,6 +69,7 @@
" "
/> />
<HoppSmartItem <HoppSmartItem
v-if="!isViewer"
ref="exportAsJsonEl" ref="exportAsJsonEl"
:icon="IconEdit" :icon="IconEdit"
:label="`${t('export.as_json')}`" :label="`${t('export.as_json')}`"
@@ -81,6 +82,7 @@
" "
/> />
<HoppSmartItem <HoppSmartItem
v-if="!isViewer"
ref="deleteAction" ref="deleteAction"
:icon="IconTrash2" :icon="IconTrash2"
:label="`${t('action.delete')}`" :label="`${t('action.delete')}`"
@@ -124,6 +126,8 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { HoppSmartItem } from "@hoppscotch/ui" import { HoppSmartItem } from "@hoppscotch/ui"
import { exportAsJSON } from "~/helpers/import-export/export/environment" import { exportAsJSON } from "~/helpers/import-export/export/environment"
import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -137,6 +141,8 @@ const emit = defineEmits<{
(e: "edit-environment"): void (e: "edit-environment"): void
}>() }>()
const secretEnvironmentService = useService(SecretEnvironmentService)
const confirmRemove = ref(false) const confirmRemove = ref(false)
const exportEnvironmentAsJSON = () => const exportEnvironmentAsJSON = () =>
@@ -161,6 +167,7 @@ const removeEnvironment = () => {
}, },
() => { () => {
toast.success(`${t("team_environment.deleted")}`) toast.success(`${t("team_environment.deleted")}`)
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
} }
) )
)() )()

View File

@@ -105,6 +105,7 @@
:editing-environment="editingEnvironment" :editing-environment="editingEnvironment"
:editing-team-id="team?.id" :editing-team-id="team?.id"
:editing-variable-name="editingVariableName" :editing-variable-name="editingVariableName"
:is-secret-option-selected="secretOptionSelected"
:is-viewer="team?.myRole === 'VIEWER'" :is-viewer="team?.myRole === 'VIEWER'"
@hide-modal="displayModalEdit(false)" @hide-modal="displayModalEdit(false)"
/> />
@@ -148,6 +149,7 @@ const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit") const action = ref<"new" | "edit">("edit")
const editingEnvironment = ref<TeamEnvironment | null>(null) const editingEnvironment = ref<TeamEnvironment | null>(null)
const editingVariableName = ref("") const editingVariableName = ref("")
const secretOptionSelected = ref(false)
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER") const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
@@ -171,6 +173,8 @@ const editEnvironment = (environment: TeamEnvironment | null) => {
} }
const resetSelectedData = () => { const resetSelectedData = () => {
editingEnvironment.value = null editingEnvironment.value = null
editingVariableName.value = ""
secretOptionSelected.value = false
} }
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
@@ -187,12 +191,15 @@ const getErrorMessage = (err: GQLError<string>) => {
defineActionHandler( defineActionHandler(
"modals.team.environment.edit", "modals.team.environment.edit",
({ envName, variableName }) => { ({ envName, variableName, isSecret }) => {
if (variableName) editingVariableName.value = variableName if (variableName) editingVariableName.value = variableName
const teamEnvToEdit = props.teamEnvironments.find( const teamEnvToEdit = props.teamEnvironments.find(
(environment) => environment.environment.name === envName (environment) => environment.environment.name === envName
) )
if (teamEnvToEdit) editEnvironment(teamEnvToEdit) if (teamEnvToEdit) {
editEnvironment(teamEnvToEdit)
secretOptionSelected.value = isSecret ?? false
}
} }
) )
</script> </script>

View File

@@ -31,17 +31,6 @@
tabindex="0" tabindex="0"
@keyup.escape="hide()" @keyup.escape="hide()"
> >
<HoppSmartItem
label="None"
:icon="authName === 'None' ? IconCircleDot : IconCircle"
:active="authName === 'None'"
@click="
() => {
auth.authType = 'none'
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
v-if="!isRootCollection" v-if="!isRootCollection"
label="Inherit" label="Inherit"
@@ -54,6 +43,17 @@
} }
" "
/> />
<HoppSmartItem
label="None"
:icon="authName === 'None' ? IconCircleDot : IconCircle"
:active="authName === 'None'"
@click="
() => {
auth.authType = 'none'
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
label="Basic Auth" label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle" :icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
@@ -284,7 +284,7 @@ const authActive = pluckRef(auth, "authActive")
const clearContent = () => { const clearContent = () => {
auth.value = { auth.value = {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
} }
} }

View File

@@ -27,9 +27,9 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlHeaders')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -315,6 +315,8 @@ import { commonHeaders } from "~/helpers/headers"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { objRemoveKey } from "~/helpers/functional/object" import { objRemoveKey } from "~/helpers/functional/object"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { HoppGQLHeader } from "~/helpers/graphql" import { HoppGQLHeader } from "~/helpers/graphql"
import { throwError } from "~/helpers/functional/error" import { throwError } from "~/helpers/functional/error"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
@@ -338,7 +340,7 @@ const request = useVModel(props, "modelValue", emit)
const idTicker = ref(0) const idTicker = ref(0)
const linewrapEnabled = ref(false) const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlHeaders")
const bulkMode = ref(false) const bulkMode = ref(false)
const bulkHeaders = ref("") const bulkHeaders = ref("")
@@ -353,7 +355,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "text/x-yaml", mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`, placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -61,9 +61,9 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlQuery')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -112,6 +112,8 @@ import {
socketDisconnect, socketDisconnect,
subscriptionState, subscriptionState,
} from "~/helpers/graphql/connection" } from "~/helpers/graphql/connection"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
// Template refs // Template refs
const queryEditor = ref<any | null>(null) const queryEditor = ref<any | null>(null)
@@ -137,7 +139,7 @@ const prettifyQueryIcon = refAutoReset<
typeof IconWand | typeof IconCheck | typeof IconInfo typeof IconWand | typeof IconCheck | typeof IconInfo
>(IconWand, 1000) >(IconWand, 1000)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlQuery")
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null) const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
@@ -184,7 +186,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "graphql", mode: "graphql",
placeholder: `${t("request.query")}`, placeholder: `${t("request.query")}`,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: createGQLQueryLinter(schema), linter: createGQLQueryLinter(schema),
completer: queryCompleter(schema), completer: queryCompleter(schema),

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="flex h-full flex-1 flex-col"> <div class="h-full">
<HoppSmartTabs <HoppSmartTabs
v-model="selectedOptionTab" v-model="selectedOptionTab"
styles="sticky top-0 bg-primary z-10 border-b-0" styles="sticky top-0 bg-primary z-10 border-b-0"

View File

@@ -16,9 +16,11 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="
toggleNestedSetting('WRAP_LINES', 'graphqlResponseBody')
"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }" v-tippy="{ theme: 'tooltip', allowHTML: true }"
@@ -70,7 +72,9 @@
</tippy> </tippy>
</div> </div>
</div> </div>
<div ref="schemaEditor" class="flex flex-1 flex-col"></div> <div class="h-full">
<div ref="schemaEditor"></div>
</div>
</div> </div>
<component <component
:is="response[0].error.component" :is="response[0].error.component"
@@ -99,6 +103,8 @@ import { useI18n } from "@composables/i18n"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { GQLResponseEvent } from "~/helpers/graphql/connection" import { GQLResponseEvent } from "~/helpers/graphql/connection"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import interfaceLanguages from "~/helpers/utils/interfaceLanguages" import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
import { import {
useCopyInterface, useCopyInterface,
@@ -133,8 +139,8 @@ const responseString = computed(() => {
}) })
const schemaEditor = ref<any | null>(null) const schemaEditor = ref<any | null>(null)
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlResponseBody")
const copyInterfaceTippyActions = ref<any | null>(null) const copyInterfaceTippyActions = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror( useCodemirror(
schemaEditor, schemaEditor,
@@ -143,7 +149,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "application/ld+json", mode: "application/ld+json",
readOnly: true, readOnly: true,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -127,9 +127,9 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlSchema')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -145,11 +145,9 @@
/> />
</div> </div>
</div> </div>
<div <div v-if="schemaString" class="h-full relative w-full">
v-if="schemaString" <div ref="schemaEditor" class="absolute inset-0"></div>
ref="schemaEditor" </div>
class="flex flex-1 flex-col"
></div>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else v-else
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
@@ -202,6 +200,8 @@ import {
subscriptionFields, subscriptionFields,
} from "~/helpers/graphql/connection" } from "~/helpers/graphql/connection"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
type NavigationTabs = "history" | "collection" | "docs" | "schema" type NavigationTabs = "history" | "collection" | "docs" | "schema"
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types" type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
@@ -349,7 +349,7 @@ const handleJumpToType = async (type: GraphQLType) => {
} }
const schemaEditor = ref<any | null>(null) const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlSchema")
useCodemirror( useCodemirror(
schemaEditor, schemaEditor,
@@ -358,7 +358,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "graphql", mode: "graphql",
readOnly: true, readOnly: true,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -49,9 +49,9 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlVariables')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -67,7 +67,9 @@
/> />
</div> </div>
</div> </div>
<div ref="variableEditor" class="flex flex-1 flex-col"></div> <div class="h-full relative">
<div ref="variableEditor" class="flex flex-1 flex-col"></div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -93,6 +95,8 @@ import {
socketDisconnect, socketDisconnect,
subscriptionState, subscriptionState,
} from "~/helpers/graphql/connection" } from "~/helpers/graphql/connection"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -114,7 +118,7 @@ const variableString = useVModel(props, "modelValue", emit)
const variableEditor = ref<any | null>(null) const variableEditor = ref<any | null>(null)
const linewrapEnabled = ref(false) const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlVariables")
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>( const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy, IconCopy,
@@ -131,7 +135,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "application/ld+json", mode: "application/ld+json",
placeholder: `${t("request.variables")}`, placeholder: `${t("request.variables")}`,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: computed(() => linter: computed(() =>
variableString.value.length > 0 ? jsonLinter : null variableString.value.length > 0 ? jsonLinter : null

View File

@@ -331,7 +331,8 @@ const deleteHistory = (entry: HistoryEntry) => {
const addToCollection = (entry: HistoryEntry) => { const addToCollection = (entry: HistoryEntry) => {
if (props.page === "rest") { if (props.page === "rest") {
invokeAction("request.save-as", { invokeAction("request.save-as", {
request: entry.request, requestType: "rest",
request: entry.request as HoppRESTRequest,
}) })
} }
} }

View File

@@ -31,17 +31,6 @@
tabindex="0" tabindex="0"
@keyup.escape="hide()" @keyup.escape="hide()"
> >
<HoppSmartItem
label="None"
:icon="authName === 'None' ? IconCircleDot : IconCircle"
:active="authName === 'None'"
@click="
() => {
auth.authType = 'none'
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
v-if="!isRootCollection" v-if="!isRootCollection"
label="Inherit" label="Inherit"
@@ -54,6 +43,17 @@
} }
" "
/> />
<HoppSmartItem
label="None"
:icon="authName === 'None' ? IconCircleDot : IconCircle"
:active="authName === 'None'"
@click="
() => {
auth.authType = 'none'
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
label="Basic Auth" label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle" :icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
@@ -265,7 +265,7 @@ const authActive = pluckRef(auth, "authActive")
const clearContent = () => { const clearContent = () => {
auth.value = { auth.value = {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
} }
} }

View File

@@ -86,9 +86,9 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'codeGen')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }" v-tippy="{ theme: 'tooltip', allowHTML: true }"
@@ -161,6 +161,8 @@ import cloneDeep from "lodash-es/cloneDeep"
import { platform } from "~/platform" import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const t = useI18n() const t = useI18n()
@@ -187,6 +189,8 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
const requestCode = computed(() => { const requestCode = computed(() => {
const aggregateEnvs = getAggregateEnvs() const aggregateEnvs = getAggregateEnvs()
const env: Environment = { const env: Environment = {
v: 1,
id: "env",
name: "Env", name: "Env",
variables: aggregateEnvs, variables: aggregateEnvs,
} }
@@ -222,7 +226,7 @@ const requestCode = computed(() => {
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
const generatedCode = ref<any | null>(null) const generatedCode = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "codeGen")
useCodemirror( useCodemirror(
generatedCode, generatedCode,
@@ -231,7 +235,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "text/plain", mode: "text/plain",
readOnly: true, readOnly: true,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -29,9 +29,9 @@
v-if="bulkMode" v-if="bulkMode"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpHeaders')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -49,7 +49,9 @@
/> />
</div> </div>
</div> </div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div> <div v-if="bulkMode" class="h-full relative w-full">
<div ref="bulkEditor" class="absolute inset-0"></div>
</div>
<div v-else> <div v-else>
<draggable <draggable
v-model="workingHeaders" v-model="workingHeaders"
@@ -332,6 +334,8 @@ import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection" import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n() const t = useI18n()
@@ -346,7 +350,7 @@ const idTicker = ref(0)
const bulkMode = ref(false) const bulkMode = ref(false)
const bulkHeaders = ref("") const bulkHeaders = ref("")
const bulkEditor = ref<any | null>(null) const bulkEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpHeaders")
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null) const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
@@ -371,7 +375,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "text/x-yaml", mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`, placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter, linter,
completer: null, completer: null,
@@ -553,7 +557,7 @@ const clearContent = () => {
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs()) const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
const computedHeaders = computed(() => const computedHeaders = computed(() =>
getComputedHeaders(request.value, aggregateEnvs.value).map( getComputedHeaders(request.value, aggregateEnvs.value, false).map(
(header, index) => ({ (header, index) => ({
id: `header-${index}`, id: `header-${index}`,
...header, ...header,
@@ -606,7 +610,8 @@ const inheritedProperties = computed(() => {
const computedAuthHeader = getComputedAuthHeaders( const computedAuthHeader = getComputedAuthHeaders(
aggregateEnvs.value, aggregateEnvs.value,
request.value, request.value,
props.inheritedProperties.auth.inheritedAuth props.inheritedProperties.auth.inheritedAuth,
false
)[0] )[0]
if ( if (

View File

@@ -22,9 +22,9 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'importCurl')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }" v-tippy="{ theme: 'tooltip', allowHTML: true }"
@@ -96,6 +96,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
import { platform } from "~/platform" import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const t = useI18n() const t = useI18n()
@@ -106,7 +108,7 @@ const tabs = useService(RESTTabService)
const curl = ref("") const curl = ref("")
const curlEditor = ref<any | null>(null) const curlEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "importCurl")
const props = defineProps<{ show: boolean; text: string }>() const props = defineProps<{ show: boolean; text: string }>()
@@ -117,7 +119,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "application/x-sh", mode: "application/x-sh",
placeholder: `${t("request.enter_curl")}`, placeholder: `${t("request.enter_curl")}`,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -3,14 +3,25 @@
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="oidcDiscoveryURL" v-model="oidcDiscoveryURL"
:styles="
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
"
placeholder="OpenID Connect Discovery URL" placeholder="OpenID Connect Discovery URL"
/> />
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="authURL" placeholder="Authorization URL" /> <SmartEnvInput
v-model="authURL"
placeholder="Authorization URL"
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
></SmartEnvInput>
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="accessTokenURL" placeholder="Access Token URL" /> <SmartEnvInput
v-model="accessTokenURL"
placeholder="Access Token URL"
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
/>
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="clientID" placeholder="Client ID" /> <SmartEnvInput v-model="clientID" placeholder="Client ID" />
@@ -44,6 +55,7 @@ import { useToast } from "@composables/toast"
import { tokenRequest } from "~/helpers/oauth" import { tokenRequest } from "~/helpers/oauth"
import { getCombinedEnvVariables } from "~/helpers/preRequest" import { getCombinedEnvVariables } from "~/helpers/preRequest"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { computed } from "vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -66,10 +78,16 @@ watch(
) )
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL") const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
const hasOIDCURL = computed(() => {
return oidcDiscoveryURL.value
})
const authURL = pluckRef(auth, "authURL") const authURL = pluckRef(auth, "authURL")
const accessTokenURL = pluckRef(auth, "accessTokenURL") const accessTokenURL = pluckRef(auth, "accessTokenURL")
const hasAccessTokenOrAuthURL = computed(() => {
return accessTokenURL.value || authURL.value
})
const clientID = pluckRef(auth, "clientID") const clientID = pluckRef(auth, "clientID")
@@ -88,13 +106,11 @@ function translateTokenRequestError(error: string) {
} }
const handleAccessTokenRequest = async () => { const handleAccessTokenRequest = async () => {
if ( if (!oidcDiscoveryURL.value && !(authURL.value || accessTokenURL.value)) {
oidcDiscoveryURL.value === "" &&
(authURL.value === "" || accessTokenURL.value === "")
) {
toast.error(`${t("error.incomplete_config_urls")}`) toast.error(`${t("error.incomplete_config_urls")}`)
return return
} }
const envs = getCombinedEnvVariables() const envs = getCombinedEnvVariables()
const envVars = [...envs.selected, ...envs.global] const envVars = [...envs.selected, ...envs.global]

View File

@@ -24,9 +24,9 @@
v-if="bulkMode" v-if="bulkMode"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpParams')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -44,7 +44,9 @@
/> />
</div> </div>
</div> </div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div> <div v-if="bulkMode" class="h-full relative">
<div ref="bulkEditor" class="absolute inset-0"></div>
</div>
<div v-else> <div v-else>
<draggable <draggable
v-model="workingParams" v-model="workingParams"
@@ -205,6 +207,8 @@ import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection" import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -217,7 +221,7 @@ const idTicker = ref(0)
const bulkMode = ref(false) const bulkMode = ref(false)
const bulkParams = ref("") const bulkParams = ref("")
const bulkEditor = ref<any | null>(null) const bulkEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpParams")
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null) const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
@@ -228,7 +232,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "text/x-yaml", mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`, placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter, linter,
completer: null, completer: null,

View File

@@ -23,15 +23,15 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpPreRequest')"
/> />
</div> </div>
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight"> <div class="w-2/3 border-r border-dividerLight h-full relative">
<div ref="preRequestEditor" class="h-full"></div> <div ref="preRequestEditor" class="h-full absolute inset-0"></div>
</div> </div>
<div <div
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4" class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
@@ -72,6 +72,8 @@ import linter from "~/helpers/editor/linting/preRequest"
import completer from "~/helpers/editor/completion/preRequest" import completer from "~/helpers/editor/completion/preRequest"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const t = useI18n() const t = useI18n()
@@ -85,7 +87,7 @@ const emit = defineEmits<{
const preRequestScript = useVModel(props, "modelValue", emit) const preRequestScript = useVModel(props, "modelValue", emit)
const preRequestEditor = ref<any | null>(null) const preRequestEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpPreRequest")
useCodemirror( useCodemirror(
preRequestEditor, preRequestEditor,
@@ -93,7 +95,7 @@ useCodemirror(
reactive({ reactive({
extendedEditorConfig: { extendedEditorConfig: {
mode: "application/javascript", mode: "application/javascript",
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
placeholder: `${t("preRequest.javascript_code")}`, placeholder: `${t("preRequest.javascript_code")}`,
}, },
linter, linter,

View File

@@ -23,9 +23,9 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpRequestBody')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-if=" v-if="
@@ -59,7 +59,9 @@
/> />
</div> </div>
</div> </div>
<div ref="rawBodyParameters" class="flex flex-1 flex-col"></div> <div class="h-full relative">
<div ref="rawBodyParameters" class="absolute inset-0"></div>
</div>
</div> </div>
</template> </template>
@@ -85,6 +87,8 @@ import { isJSONContentType } from "~/helpers/utils/contenttypes"
import jsonLinter from "~/helpers/editor/linting/json" import jsonLinter from "~/helpers/editor/linting/json"
import { readFileAsText } from "~/helpers/functional/files" import { readFileAsText } from "~/helpers/functional/files"
import xmlFormat from "xml-formatter" import xmlFormat from "xml-formatter"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
type PossibleContentTypes = Exclude< type PossibleContentTypes = Exclude<
ValidContentTypes, ValidContentTypes,
@@ -122,7 +126,7 @@ const langLinter = computed(() =>
isJSONContentType(body.value.contentType) ? jsonLinter : null isJSONContentType(body.value.contentType) ? jsonLinter : null
) )
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpRequestBody")
const rawBodyParameters = ref<any | null>(null) const rawBodyParameters = ref<any | null>(null)
const codemirrorValue: Ref<string | undefined> = const codemirrorValue: Ref<string | undefined> =
@@ -148,7 +152,7 @@ useCodemirror(
codemirrorValue, codemirrorValue,
reactive({ reactive({
extendedEditorConfig: { extendedEditorConfig: {
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
mode: rawInputEditorLang, mode: rawInputEditorLang,
placeholder: t("request.raw_body").toString(), placeholder: t("request.raw_body").toString(),
}, },

View File

@@ -255,7 +255,7 @@ import IconShare2 from "~icons/lucide/share-2"
import { getDefaultRESTRequest } from "~/helpers/rest/default" import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history" import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
import { platform } from "~/platform" import { platform } from "~/platform"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data" import { HoppRESTRequest } from "@hoppscotch/data"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection" import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
@@ -263,6 +263,7 @@ import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document" import { HoppRESTDocument } from "~/helpers/rest/document"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { getMethodLabelColor } from "~/helpers/rest/labelColoring" import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
import { WorkspaceService } from "~/services/workspace.service"
const t = useI18n() const t = useI18n()
const interceptorService = useService(InterceptorService) const interceptorService = useService(InterceptorService)
@@ -326,6 +327,8 @@ const inspectionService = useService(InspectionService)
const tabs = useService(RESTTabService) const tabs = useService(RESTTabService)
const workspaceService = useService(WorkspaceService)
const newSendRequest = async () => { const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) { if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`) toast.error(`${t("empty.endpoint")}`)
@@ -341,6 +344,7 @@ const newSendRequest = async () => {
type: "HOPP_REQUEST_RUN", type: "HOPP_REQUEST_RUN",
platform: "rest", platform: "rest",
strategy: interceptorService.currentInterceptorID.value!, strategy: interceptorService.currentInterceptorID.value!,
workspaceType: workspaceService.currentWorkspace.value.type,
}) })
const [cancel, streamPromise] = runRESTRequest$(tab) const [cancel, streamPromise] = runRESTRequest$(tab)
@@ -395,17 +399,14 @@ const newSendRequest = async () => {
} }
const ensureMethodInEndpoint = () => { const ensureMethodInEndpoint = () => {
if ( const endpoint = newEndpoint.value.trim()
!/^http[s]?:\/\//.test(newEndpoint.value) && tab.value.document.request.endpoint = endpoint
!newEndpoint.value.startsWith("<<") if (!/^http[s]?:\/\//.test(endpoint) && !endpoint.startsWith("<<")) {
) { const domain = endpoint.split(/[/:#?]+/)[0]
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) { if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
tab.value.document.request.endpoint = tab.value.document.request.endpoint = "http://" + endpoint
"http://" + tab.value.document.request.endpoint
} else { } else {
tab.value.document.request.endpoint = tab.value.document.request.endpoint = "https://" + endpoint
"https://" + tab.value.document.request.endpoint
} }
} }
} }
@@ -577,25 +578,12 @@ defineActionHandler("request.share-request", shareRequest)
defineActionHandler("request.method.next", cycleDownMethod) defineActionHandler("request.method.next", cycleDownMethod)
defineActionHandler("request.method.prev", cycleUpMethod) defineActionHandler("request.method.prev", cycleUpMethod)
defineActionHandler("request.save", saveRequest) defineActionHandler("request.save", saveRequest)
defineActionHandler( defineActionHandler("request.save-as", (req) => {
"request.save-as", showSaveRequestModal.value = true
( if (req?.requestType === "rest") {
req: request.value = req.request
| {
requestType: "rest"
request: HoppRESTRequest
}
| {
requestType: "gql"
request: HoppGQLRequest
}
) => {
showSaveRequestModal.value = true
if (req && req.requestType === "rest") {
request.value = req.request
}
} }
) })
defineActionHandler("request.method.get", () => updateMethod("GET")) defineActionHandler("request.method.get", () => updateMethod("GET"))
defineActionHandler("request.method.post", () => updateMethod("POST")) defineActionHandler("request.method.post", () => updateMethod("POST"))
defineActionHandler("request.method.put", () => updateMethod("PUT")) defineActionHandler("request.method.put", () => updateMethod("PUT"))

View File

@@ -9,7 +9,7 @@
/> />
</template> </template>
<template #secondary> <template #secondary>
<HttpResponse v-model:document="tab.document" /> <HttpResponse v-model:document="tab.document" :is-embed="false" />
</template> </template>
</AppPaneLayout> </AppPaneLayout>
</template> </template>

View File

@@ -29,6 +29,7 @@
class="flex flex-col focus:outline-none" class="flex flex-col focus:outline-none"
tabindex="0" tabindex="0"
@keyup.r="renameAction?.$el.click()" @keyup.r="renameAction?.$el.click()"
@keyup.s="shareRequestAction?.$el.click()"
@keyup.d="duplicateAction?.$el.click()" @keyup.d="duplicateAction?.$el.click()"
@keyup.w="closeAction?.$el.click()" @keyup.w="closeAction?.$el.click()"
@keyup.x="closeOthersAction?.$el.click()" @keyup.x="closeOthersAction?.$el.click()"
@@ -58,6 +59,18 @@
} }
" "
/> />
<HoppSmartItem
ref="shareRequestAction"
:icon="IconShare2"
:label="t('tab.share_tab_request')"
:shortcut="['S']"
@click="
() => {
emit('share-tab-request')
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
v-if="isRemovable" v-if="isRemovable"
ref="closeAction" ref="closeAction"
@@ -99,6 +112,7 @@ import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square" import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit" import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import IconShare2 from "~icons/lucide/share-2"
import { HoppTab } from "~/services/tab" import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document" import { HoppRESTDocument } from "~/helpers/rest/document"
@@ -114,6 +128,7 @@ const emit = defineEmits<{
(event: "close-tab"): void (event: "close-tab"): void
(event: "close-other-tabs"): void (event: "close-other-tabs"): void
(event: "duplicate-tab"): void (event: "duplicate-tab"): void
(event: "share-tab-request"): void
}>() }>()
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
@@ -123,4 +138,5 @@ const renameAction = ref<HTMLButtonElement | null>(null)
const closeAction = ref<HTMLButtonElement | null>(null) const closeAction = ref<HTMLButtonElement | null>(null)
const closeOthersAction = ref<HTMLButtonElement | null>(null) const closeOthersAction = ref<HTMLButtonElement | null>(null)
const duplicateAction = ref<HTMLButtonElement | null>(null) const duplicateAction = ref<HTMLButtonElement | null>(null)
const shareRequestAction = ref<HTMLButtonElement | null>(null)
</script> </script>

View File

@@ -211,7 +211,6 @@ import { useI18n } from "@composables/i18n"
import { import {
globalEnv$, globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setGlobalEnvVariables,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import { HoppTestResult } from "~/helpers/types/HoppTestResult" import { HoppTestResult } from "~/helpers/types/HoppTestResult"
@@ -225,6 +224,7 @@ import { useColorMode } from "~/composables/theming"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { invokeAction } from "~/helpers/actions"
const props = defineProps<{ const props = defineProps<{
modelValue: HoppTestResult | null | undefined modelValue: HoppTestResult | null | undefined
@@ -304,9 +304,10 @@ const globalHasAdditions = computed(() => {
const addEnvToGlobal = () => { const addEnvToGlobal = () => {
if (!testResults.value?.envDiff.selected.additions) return if (!testResults.value?.envDiff.selected.additions) return
setGlobalEnvVariables([
...globalEnvVars.value, invokeAction("modals.global.environment.update", {
...testResults.value.envDiff.selected.additions, variables: testResults.value.envDiff.selected.additions,
]) isSecret: false,
})
} }
</script> </script>

View File

@@ -23,15 +23,15 @@
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpTest')"
/> />
</div> </div>
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight"> <div class="w-2/3 border-r border-dividerLight h-full relative">
<div ref="testScriptEditor" class="h-full"></div> <div ref="testScriptEditor" class="h-full absolute inset-0"></div>
</div> </div>
<div <div
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4" class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
@@ -72,6 +72,8 @@ import linter from "~/helpers/editor/linting/testScript"
import completer from "~/helpers/editor/completion/testScript" import completer from "~/helpers/editor/completion/testScript"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const t = useI18n() const t = useI18n()
@@ -81,7 +83,7 @@ const props = defineProps<{
const emit = defineEmits(["update:modelValue"]) const emit = defineEmits(["update:modelValue"])
const testScript = useVModel(props, "modelValue", emit) const testScript = useVModel(props, "modelValue", emit)
const testScriptEditor = ref<any | null>(null) const testScriptEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpTest")
useCodemirror( useCodemirror(
testScriptEditor, testScriptEditor,
@@ -89,7 +91,7 @@ useCodemirror(
reactive({ reactive({
extendedEditorConfig: { extendedEditorConfig: {
mode: "application/javascript", mode: "application/javascript",
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
placeholder: `${t("test.javascript_code")}`, placeholder: `${t("test.javascript_code")}`,
}, },
linter, linter,

View File

@@ -24,9 +24,9 @@
v-if="bulkMode" v-if="bulkMode"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpUrlEncoded')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -44,7 +44,9 @@
/> />
</div> </div>
</div> </div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div> <div v-if="bulkMode" class="h-full relative">
<div ref="bulkEditor" class="absolute inset-0"></div>
</div>
<div v-else> <div v-else>
<draggable <draggable
v-model="workingUrlEncodedParams" v-model="workingUrlEncodedParams"
@@ -196,6 +198,8 @@ import { useColorMode } from "@composables/theming"
import { objRemoveKey } from "~/helpers/functional/object" import { objRemoveKey } from "~/helpers/functional/object"
import { throwError } from "~/helpers/functional/error" import { throwError } from "~/helpers/functional/error"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
type Body = HoppRESTReqBody & { type Body = HoppRESTReqBody & {
contentType: "application/x-www-form-urlencoded" contentType: "application/x-www-form-urlencoded"
@@ -220,7 +224,7 @@ const idTicker = ref(0)
const bulkMode = ref(false) const bulkMode = ref(false)
const bulkUrlEncodedParams = ref("") const bulkUrlEncodedParams = ref("")
const bulkEditor = ref<any | null>(null) const bulkEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpUrlEncoded")
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null) const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
@@ -231,7 +235,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "text/x-yaml", mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`, placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter, linter,
completer: null, completer: null,

View File

@@ -11,9 +11,9 @@
v-if="response.body" v-if="response.body"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-if="response.body" v-if="response.body"
@@ -44,11 +44,9 @@
/> />
</div> </div>
</div> </div>
<div <div v-show="!previewEnabled" class="h-full">
v-show="!previewEnabled" <div ref="htmlResponse" class="flex flex-1 flex-col"></div>
ref="htmlResponse" </div>
class="flex flex-1 flex-col"
></div>
<iframe <iframe
v-show="previewEnabled" v-show="previewEnabled"
ref="previewFrame" ref="previewFrame"
@@ -76,6 +74,8 @@ import { useI18n } from "@composables/i18n"
import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const t = useI18n() const t = useI18n()
@@ -84,7 +84,7 @@ const props = defineProps<{
}>() }>()
const htmlResponse = ref<any | null>(null) const htmlResponse = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
const { responseBodyText } = useResponseBody(props.response) const { responseBodyText } = useResponseBody(props.response)
const { downloadIcon, downloadResponse } = useDownloadResponse( const { downloadIcon, downloadResponse } = useDownloadResponse(
@@ -104,7 +104,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "htmlmixed", mode: "htmlmixed",
readOnly: true, readOnly: true,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -14,9 +14,9 @@
v-if="response.body" v-if="response.body"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-if="response.body" v-if="response.body"
@@ -119,11 +119,12 @@
/> />
</div> </div>
</div> </div>
<div <div class="h-full">
ref="jsonResponse" <div
class="flex h-auto h-full flex-1 flex-col" ref="jsonResponse"
:class="toggleFilter ? 'responseToggleOn' : 'responseToggleOff'" :class="toggleFilter ? 'responseToggleOn' : 'responseToggleOff'"
></div> ></div>
</div>
<div <div
v-if="outlinePath" v-if="outlinePath"
class="sticky bottom-0 z-10 flex flex-shrink-0 flex-nowrap overflow-auto overflow-x-auto border-t border-dividerLight bg-primaryLight px-2" class="sticky bottom-0 z-10 flex flex-shrink-0 flex-nowrap overflow-auto overflow-x-auto border-t border-dividerLight bg-primaryLight px-2"
@@ -260,6 +261,8 @@ import {
} from "@composables/lens-actions" } from "@composables/lens-actions"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import interfaceLanguages from "~/helpers/utils/interfaceLanguages" import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
const t = useI18n() const t = useI18n()
@@ -371,8 +374,8 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
const jsonResponse = ref<any | null>(null) const jsonResponse = ref<any | null>(null)
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
const copyInterfaceTippyActions = ref<any | null>(null) const copyInterfaceTippyActions = ref<any | null>(null)
const linewrapEnabled = ref(true)
const { cursor } = useCodemirror( const { cursor } = useCodemirror(
jsonResponse, jsonResponse,
@@ -381,7 +384,7 @@ const { cursor } = useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "application/ld+json", mode: "application/ld+json",
readOnly: true, readOnly: true,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -11,9 +11,9 @@
v-if="response.body" v-if="response.body"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-if="response.body" v-if="response.body"
@@ -35,7 +35,9 @@
/> />
</div> </div>
</div> </div>
<div ref="rawResponse" class="flex flex-1 flex-col"></div> <div class="h-full">
<div ref="rawResponse" class="flex flex-1 flex-col"></div>
</div>
</div> </div>
</template> </template>
@@ -58,6 +60,8 @@ import {
import { objFieldMatches } from "~/helpers/functional/object" import { objFieldMatches } from "~/helpers/functional/object"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const t = useI18n() const t = useI18n()
@@ -97,7 +101,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText) const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const rawResponse = ref<any | null>(null) const rawResponse = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
useCodemirror( useCodemirror(
rawResponse, rawResponse,
@@ -106,7 +110,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "text/plain", mode: "text/plain",
readOnly: true, readOnly: true,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -11,9 +11,9 @@
v-if="response.body" v-if="response.body"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')" :title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }" :class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText" :icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled" @click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-if="response.body" v-if="response.body"
@@ -35,7 +35,9 @@
/> />
</div> </div>
</div> </div>
<div ref="xmlResponse" class="flex flex-1 flex-col"></div> <div class="h-full">
<div ref="xmlResponse" class="flex flex-1 flex-col"></div>
</div>
</div> </div>
</template> </template>
@@ -58,6 +60,8 @@ import {
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { objFieldMatches } from "~/helpers/functional/object" import { objFieldMatches } from "~/helpers/functional/object"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
const t = useI18n() const t = useI18n()
@@ -91,7 +95,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText) const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
const xmlResponse = ref<any | null>(null) const xmlResponse = ref<any | null>(null)
const linewrapEnabled = ref(true) const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
useCodemirror( useCodemirror(
xmlResponse, xmlResponse,
@@ -100,7 +104,7 @@ useCodemirror(
extendedEditorConfig: { extendedEditorConfig: {
mode: "application/xml", mode: "application/xml",
readOnly: true, readOnly: true,
lineWrapping: linewrapEnabled, lineWrapping: WRAP_LINES,
}, },
linter: null, linter: null,
completer: null, completer: null,

View File

@@ -130,7 +130,9 @@
/> />
</div> </div>
</div> </div>
<div ref="wsCommunicationBody" class="flex flex-1 flex-col"></div> <div class="h-full">
<div ref="wsCommunicationBody" class="flex flex-1 flex-col"></div>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -7,7 +7,7 @@
v-tippy="{ theme: 'tooltip', delay: [500, 20] }" v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="flex items-center justify-center flex-1 min-w-0 py-2 cursor-pointer pointer-events-auto" class="flex items-center justify-center flex-1 min-w-0 py-2 cursor-pointer pointer-events-auto"
:title="`${timeStamp}`" :title="`${timeStamp}`"
@click="openInNewTab" @click="customizeSharedRequest()"
> >
<span <span
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none" class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
@@ -62,7 +62,7 @@
:shortcut="['T']" :shortcut="['T']"
@click=" @click="
() => { () => {
openInNewTab() emit('open-shared-request', parseRequest)
hide() hide()
} }
" "
@@ -128,7 +128,7 @@ const emit = defineEmits<{
embedProperties?: string | null embedProperties?: string | null
): void ): void
(e: "delete-shared-request", codeID: string): void (e: "delete-shared-request", codeID: string): void
(e: "open-new-tab", request: HoppRESTRequest): void (e: "open-shared-request", request: HoppRESTRequest): void
}>() }>()
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
@@ -145,10 +145,6 @@ const requestLabelColor = computed(() =>
getMethodLabelColorClassOf(parseRequest.value) getMethodLabelColorClassOf(parseRequest.value)
) )
const openInNewTab = () => {
emit("open-new-tab", parseRequest.value)
}
const customizeSharedRequest = () => { const customizeSharedRequest = () => {
const embedProperties = props.request.properties const embedProperties = props.request.properties
emit( emit(

View File

@@ -9,8 +9,14 @@
/> />
</div> </div>
<div <div
class="sticky top-sidebarPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-end overflow-x-auto border-b border-dividerLight bg-primary" class="sticky top-sidebarPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
> >
<HoppButtonSecondary
:label="t('action.new')"
:icon="IconPlus"
class="!rounded-none"
@click="shareRequest()"
/>
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/widgets" to="https://docs.hoppscotch.io/documentation/features/widgets"
@@ -47,7 +53,7 @@
:request="request" :request="request"
@customize-shared-request="customizeSharedRequest" @customize-shared-request="customizeSharedRequest"
@delete-shared-request="deleteSharedRequest" @delete-shared-request="deleteSharedRequest"
@open-new-tab="openInNewTab" @open-shared-request="openRequestInNewTab"
/> />
<HoppSmartIntersection <HoppSmartIntersection
v-if="hasMoreSharedRequests" v-if="hasMoreSharedRequests"
@@ -70,7 +76,15 @@
:alt="`${t('empty.shared_requests')}`" :alt="`${t('empty.shared_requests')}`"
:text="t('empty.shared_requests')" :text="t('empty.shared_requests')"
@drop.stop @drop.stop
/> >
<template #body>
<HoppButtonPrimary
:label="t('add.new')"
:icon="IconPlus"
@click="shareRequest()"
/>
</template>
</HoppSmartPlaceholder>
</div> </div>
</div> </div>
<HoppSmartConfirmModal <HoppSmartConfirmModal
@@ -95,6 +109,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconPlus from "~icons/lucide/plus"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import ShortcodeListAdapter from "~/helpers/shortcode/ShortcodeListAdapter" import ShortcodeListAdapter from "~/helpers/shortcode/ShortcodeListAdapter"
import { useReadonlyStream } from "~/composables/stream" import { useReadonlyStream } from "~/composables/stream"
@@ -270,6 +285,17 @@ onAuthEvent((ev) => {
} }
}) })
const shareRequest = () => {
if (currentUser.value) {
const tab = restTab.currentActiveTab
invokeAction("share.request", {
request: tab.value.document.request,
})
} else {
invokeAction("modals.login.toggle")
}
}
const deleteSharedRequest = (codeID: string) => { const deleteSharedRequest = (codeID: string) => {
if (currentUser.value) { if (currentUser.value) {
sharedRequestID.value = codeID sharedRequestID.value = codeID
@@ -434,13 +460,6 @@ const copySharedRequest = (payload: {
} }
} }
const openInNewTab = (request: HoppRESTRequest) => {
restTab.createNewTab({
isDirty: false,
request,
})
}
const resolveConfirmModal = (title: string | null) => { const resolveConfirmModal = (title: string | null) => {
if (title === `${t("confirm.remove_shared_request")}`) onDeleteSharedRequest() if (title === `${t("confirm.remove_shared_request")}`) onDeleteSharedRequest()
else { else {
@@ -465,6 +484,13 @@ const getErrorMessage = (err: GQLError<string>) => {
} }
} }
const openRequestInNewTab = (request: HoppRESTRequest) => {
restTab.createNewTab({
isDirty: false,
request,
})
}
defineActionHandler("share.request", ({ request }) => { defineActionHandler("share.request", ({ request }) => {
requestToShare.value = request requestToShare.value = request
displayShareRequestModal(true) displayShareRequestModal(true)

View File

@@ -3,7 +3,18 @@
<div <div
class="no-scrollbar absolute inset-0 flex flex-1 divide-x divide-dividerLight overflow-x-auto" class="no-scrollbar absolute inset-0 flex flex-1 divide-x divide-dividerLight overflow-x-auto"
> >
<input
v-if="isSecret"
id="secret"
v-model="secretText"
name="secret"
:placeholder="t('environment.secret_value')"
class="flex flex-1 bg-transparent px-4"
:class="styles"
type="password"
/>
<div <div
v-else
ref="editor" ref="editor"
:placeholder="placeholder" :placeholder="placeholder"
class="flex flex-1" class="flex flex-1"
@@ -11,7 +22,14 @@
@click="emit('click', $event)" @click="emit('click', $event)"
@keydown="handleKeystroke" @keydown="handleKeystroke"
@focusin="showSuggestionPopover = true" @focusin="showSuggestionPopover = true"
></div> />
<HoppButtonSecondary
v-if="secret"
v-tippy="{ theme: 'tooltip' }"
:title="isSecret ? t('action.show_secret') : t('action.hide_secret')"
:icon="isSecret ? IconEyeoff : IconEye"
@click="toggleSecret"
/>
<AppInspection <AppInspection
:inspection-results="inspectionResults" :inspection-results="inspectionResults"
class="sticky inset-y-0 right-0 rounded-r bg-primary" class="sticky inset-y-0 right-0 rounded-r bg-primary"
@@ -61,18 +79,29 @@ import { history, historyKeymap } from "@codemirror/commands"
import { inputTheme } from "~/helpers/editor/themes/baseTheme" import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment" import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments" import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
} from "~/newstore/environments"
import { platform } from "~/platform" import { platform } from "~/platform"
import { onClickOutside, useDebounceFn } from "@vueuse/core" import { onClickOutside, useDebounceFn } from "@vueuse/core"
import { InspectorResult } from "~/services/inspection" import { InspectorResult } from "~/services/inspection"
import { invokeAction } from "~/helpers/actions" import { invokeAction } from "~/helpers/actions"
import { Environment } from "@hoppscotch/data"
import { useI18n } from "~/composables/i18n"
import IconEye from "~icons/lucide/eye"
import IconEyeoff from "~icons/lucide/eye-off"
const t = useI18n()
type Env = Environment["variables"][number] & { source: string }
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue?: string modelValue?: string
placeholder?: string placeholder?: string
styles?: string styles?: string
envs?: { key: string; value: string; source: string }[] | null envs?: Env[] | null
focus?: boolean focus?: boolean
selectTextOnMount?: boolean selectTextOnMount?: boolean
environmentHighlights?: boolean environmentHighlights?: boolean
@@ -80,6 +109,7 @@ const props = withDefaults(
autoCompleteSource?: string[] autoCompleteSource?: string[]
inspectionResults?: InspectorResult[] | undefined inspectionResults?: InspectorResult[] | undefined
contextMenuEnabled?: boolean contextMenuEnabled?: boolean
secret?: boolean
}>(), }>(),
{ {
modelValue: "", modelValue: "",
@@ -93,6 +123,7 @@ const props = withDefaults(
inspectionResult: undefined, inspectionResult: undefined,
inspectionResults: undefined, inspectionResults: undefined,
contextMenuEnabled: true, contextMenuEnabled: true,
secret: false,
} }
) )
@@ -118,10 +149,27 @@ const showSuggestionPopover = ref(false)
const suggestionsMenu = ref<any | null>(null) const suggestionsMenu = ref<any | null>(null)
const autoCompleteWrapper = ref<any | null>(null) const autoCompleteWrapper = ref<any | null>(null)
const isSecret = ref(props.secret)
const secretText = ref(props.modelValue)
watch(
() => secretText.value,
(newVal) => {
if (isSecret.value) {
updateModelValue(newVal)
}
}
)
onClickOutside(autoCompleteWrapper, () => { onClickOutside(autoCompleteWrapper, () => {
showSuggestionPopover.value = false showSuggestionPopover.value = false
}) })
const toggleSecret = () => {
isSecret.value = !isSecret.value
}
//filter autocompleteSource with unique values //filter autocompleteSource with unique values
const uniqueAutoCompleteSource = computed(() => { const uniqueAutoCompleteSource = computed(() => {
if (props.autoCompleteSource) { if (props.autoCompleteSource) {
@@ -169,8 +217,6 @@ watch(
) )
const handleKeystroke = (ev: KeyboardEvent) => { const handleKeystroke = (ev: KeyboardEvent) => {
if (!props.autoCompleteSource) return
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(ev.key)) { if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(ev.key)) {
ev.preventDefault() ev.preventDefault()
} }
@@ -307,19 +353,28 @@ watch(
let clipboardEv: ClipboardEvent | null = null let clipboardEv: ClipboardEvent | null = null
let pastedValue: string | null = null let pastedValue: string | null = null
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref< const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
AggregateEnvironment[] AggregateEnvironment[]
> >
const envVars = computed(() => const envVars = computed(() => {
props.envs return props.envs
? props.envs.map((x) => ({ ? props.envs.map((x) => {
key: x.key, if (x.secret) {
value: x.value, return {
sourceEnv: x.source, key: x.key,
})) sourceEnv: "source" in x ? x.source : null,
value: "********",
}
}
return {
key: x.key,
value: x.value,
sourceEnv: "source" in x ? x.source : null,
}
})
: aggregateEnvs.value : aggregateEnvs.value
) })
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view) const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
@@ -363,17 +418,28 @@ const initView = (el: any) => {
el.addEventListener("keyup", debounceFn) el.addEventListener("keyup", debounceFn)
} }
const extensions: Extension = getExtensions(props.readonly || isSecret.value)
view.value = new EditorView({
parent: el,
state: EditorState.create({
doc: props.modelValue,
extensions,
}),
})
}
const getExtensions = (readonly: boolean): Extension => {
const extensions: Extension = [ const extensions: Extension = [
EditorView.contentAttributes.of({ "aria-label": props.placeholder }), EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }), EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (props.readonly) { if (readonly) {
update.view.contentDOM.inputMode = "none" update.view.contentDOM.inputMode = "none"
} }
}), }),
EditorState.changeFilter.of(() => !props.readonly), EditorState.changeFilter.of(() => !readonly),
inputTheme, inputTheme,
props.readonly readonly
? EditorView.theme({ ? EditorView.theme({
".cm-content": { ".cm-content": {
caretColor: "var(--secondary-dark-color)", caretColor: "var(--secondary-dark-color)",
@@ -384,6 +450,7 @@ const initView = (el: any) => {
}) })
: EditorView.theme({}), : EditorView.theme({}),
tooltips({ tooltips({
parent: document.body,
position: "absolute", position: "absolute",
}), }),
props.environmentHighlights ? envTooltipPlugin : [], props.environmentHighlights ? envTooltipPlugin : [],
@@ -405,7 +472,8 @@ const initView = (el: any) => {
ViewPlugin.fromClass( ViewPlugin.fromClass(
class { class {
update(update: ViewUpdate) { update(update: ViewUpdate) {
if (props.readonly) return if (readonly) return
if (update.docChanged) { if (update.docChanged) {
const prevValue = clone(cachedValue.value) const prevValue = clone(cachedValue.value)
@@ -454,14 +522,7 @@ const initView = (el: any) => {
history(), history(),
keymap.of([...historyKeymap]), keymap.of([...historyKeymap]),
] ]
return extensions
view.value = new EditorView({
parent: el,
state: EditorState.create({
doc: props.modelValue,
extensions,
}),
})
} }
const triggerTextSelection = () => { const triggerTextSelection = () => {
@@ -474,11 +535,11 @@ const triggerTextSelection = () => {
}) })
}) })
} }
onMounted(() => { onMounted(() => {
if (editor.value) { if (editor.value) {
if (!view.value) initView(editor.value) if (!view.value) initView(editor.value)
if (props.selectTextOnMount) triggerTextSelection() if (props.selectTextOnMount) triggerTextSelection()
if (props.focus) view.value?.focus()
platform.ui?.onCodemirrorInstanceMount?.(editor.value) platform.ui?.onCodemirrorInstanceMount?.(editor.value)
} }
}) })

View File

@@ -4,6 +4,7 @@ import {
ViewPlugin, ViewPlugin,
ViewUpdate, ViewUpdate,
placeholder, placeholder,
tooltips,
} from "@codemirror/view" } from "@codemirror/view"
import { import {
Extension, Extension,
@@ -269,6 +270,7 @@ export function useCodemirror(
basicSetup, basicSetup,
baseTheme, baseTheme,
syntaxHighlighting(baseHighlightStyle, { fallback: true }), syntaxHighlighting(baseHighlightStyle, { fallback: true }),
ViewPlugin.fromClass( ViewPlugin.fromClass(
class { class {
update(update: ViewUpdate) { update(update: ViewUpdate) {
@@ -318,6 +320,7 @@ export function useCodemirror(
} }
} }
), ),
EditorView.domEventHandlers({ EditorView.domEventHandlers({
scroll(event) { scroll(event) {
if (event.target && options.contextMenuEnabled) { if (event.target && options.contextMenuEnabled) {
@@ -359,6 +362,10 @@ export function useCodemirror(
run: indentLess, run: indentLess,
}, },
]), ]),
tooltips({
parent: document.body,
position: "absolute",
}),
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }), EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
additionalExts.of(options.additionalExts ?? []), additionalExts.of(options.additionalExts ?? []),
] ]

View File

@@ -13,7 +13,36 @@ export function useSetting<K extends keyof SettingsDef>(
settingsStore.dispatch({ settingsStore.dispatch({
dispatcher: "applySetting", dispatcher: "applySetting",
payload: { payload: {
// @ts-expect-error TS is not able to understand the type semantics here
settingKey, settingKey,
// @ts-expect-error TS is not able to understand the type semantics here
value,
},
})
}
)
}
export function useNestedSetting<
K extends keyof SettingsDef,
P extends keyof SettingsDef[K],
>(settingKey: K, property: P): Ref<SettingsDef[K][P]> {
return useStream(
settingsStore.subject$.pipe(
pluck(settingKey),
pluck(property),
distinctUntilChanged()
),
settingsStore.value[settingKey][property],
(value: SettingsDef[K][P]) => {
settingsStore.dispatch({
dispatcher: "applyNestedSetting",
payload: {
// @ts-expect-error TS is not able to understand the type semantics here
settingKey,
// @ts-expect-error TS is not able to understand the type semantics here
property,
// @ts-expect-error TS is not able to understand the type semantics here
value, value,
}, },
}) })
@@ -35,7 +64,9 @@ export function useSettingStatic<K extends keyof SettingsDef>(
settingsStore.dispatch({ settingsStore.dispatch({
dispatcher: "applySetting", dispatcher: "applySetting",
payload: { payload: {
// @ts-expect-error TS is not able to understand the type semantics here
settingKey, settingKey,
// @ts-expect-error TS is not able to understand the type semantics here
value, value,
}, },
}) })

View File

@@ -30,6 +30,13 @@ import { HoppRESTResponse } from "./types/HoppRESTResponse"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult" import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { getEffectiveRESTRequest } from "./utils/EffectiveURL" import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
import { isJSONContentType } from "./utils/contenttypes" import { isJSONContentType } from "./utils/contenttypes"
import {
SecretEnvironmentService,
SecretVariable,
} from "~/services/secret-environment.service"
import { getService } from "~/modules/dioc"
const secretEnvironmentService = getService(SecretEnvironmentService)
const getTestableBody = ( const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" } res: HoppRESTResponse & { type: "success" | "fail" }
@@ -58,15 +65,63 @@ const getTestableBody = (
return x return x
} }
const combineEnvVariables = (env: { const combineEnvVariables = (envs: {
global: Environment["variables"] global: Environment["variables"]
selected: Environment["variables"] selected: Environment["variables"]
}) => [...env.selected, ...env.global] }) => [...envs.selected, ...envs.global]
export const executedResponses$ = new Subject< export const executedResponses$ = new Subject<
HoppRESTResponse & { type: "success" | "fail " } HoppRESTResponse & { type: "success" | "fail " }
>() >()
/**
* Used to update the environment schema with the secret variables
* and store the secret variable values in the secret environment service
* @param envs The environment variables to update
* @param type Whether the environment variables are global or selected
* @returns the updated environment variables
*/
const updateEnvironmentsWithSecret = (
envs: Environment["variables"] &
{
secret: true
value: string | undefined
key: string
}[],
type: "global" | "selected"
) => {
const currentEnvID =
type === "selected" ? getCurrentEnvironment().id : "Global"
const updatedSecretEnvironments: SecretVariable[] = []
const updatedEnv = pipe(
envs,
A.mapWithIndex((index, e) => {
if (e.secret) {
updatedSecretEnvironments.push({
key: e.key,
value: e.value ?? "",
varIndex: index,
})
// delete the value from the environment
// so that it doesn't get saved in the environment
delete e.value
return e
}
return e
})
)
if (currentEnvID) {
secretEnvironmentService.addSecretEnvironment(
currentEnvID,
updatedSecretEnvironments
)
}
return updatedEnv
}
export function runRESTRequest$( export function runRESTRequest$(
tab: Ref<HoppTab<HoppRESTDocument>> tab: Ref<HoppTab<HoppRESTDocument>>
): [ ): [
@@ -154,15 +209,36 @@ export function runRESTRequest$(
) )
if (E.isRight(runResult)) { if (E.isRight(runResult)) {
const updatedGlobalEnvVariables = updateEnvironmentsWithSecret(
cloneDeep(runResult.right.envs.global),
"global"
)
const updatedSelectedEnvVariables = updateEnvironmentsWithSecret(
cloneDeep(runResult.right.envs.selected),
"selected"
)
// set the response in the tab so that multiple tabs can run request simultaneously // set the response in the tab so that multiple tabs can run request simultaneously
tab.value.document.response = res tab.value.document.response = res
tab.value.document.testResults = translateToSandboxTestResults( const updatedRunResult = {
runResult.right ...runResult.right,
envs: {
global: updatedGlobalEnvVariables,
selected: updatedSelectedEnvVariables,
},
}
tab.value.document.testResults =
translateToSandboxTestResults(updatedRunResult)
setGlobalEnvVariables(
updateEnvironmentsWithSecret(
runResult.right.envs.global,
"global"
)
) )
setGlobalEnvVariables(runResult.right.envs.global)
if ( if (
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV" environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
) { ) {
@@ -173,8 +249,10 @@ export function runRESTRequest$(
updateEnvironment( updateEnvironment(
environmentsStore.value.selectedEnvironmentIndex.index, environmentsStore.value.selectedEnvironmentIndex.index,
{ {
...env, name: env.name,
variables: runResult.right.envs.selected, v: 1,
id: env.id ?? "",
variables: updatedRunResult.envs.selected,
} }
) )
} else if ( } else if (
@@ -186,7 +264,7 @@ export function runRESTRequest$(
}) })
pipe( pipe(
updateTeamEnvironment( updateTeamEnvironment(
JSON.stringify(runResult.right.envs.selected), JSON.stringify(updatedRunResult.envs.selected),
environmentsStore.value.selectedEnvironmentIndex.teamEnvID, environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
env.name env.name
) )
@@ -275,7 +353,6 @@ function translateToSandboxTestResults(
const globals = cloneDeep(getGlobalVariables()) const globals = cloneDeep(getGlobalVariables())
const env = getCurrentEnvironment() const env = getCurrentEnvironment()
return { return {
description: "", description: "",
expectResults: testDesc.tests.expectResults, expectResults: testDesc.tests.expectResults,

View File

@@ -5,7 +5,7 @@
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue" import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
import { BehaviorSubject } from "rxjs" import { BehaviorSubject } from "rxjs"
import { HoppRESTDocument } from "./rest/document" import { HoppRESTDocument } from "./rest/document"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data" import { Environment, HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue" import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
import { HoppGQLSaveContext } from "./graphql/document" import { HoppGQLSaveContext } from "./graphql/document"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue" import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
@@ -43,6 +43,7 @@ export type HoppAction =
| "modals.environment.new" // Add new environment | "modals.environment.new" // Add new environment
| "modals.environment.delete-selected" // Delete Selected Environment | "modals.environment.delete-selected" // Delete Selected Environment
| "modals.my.environment.edit" // Edit current personal environment | "modals.my.environment.edit" // Edit current personal environment
| "modals.global.environment.update" // Update global environment
| "modals.team.environment.edit" // Edit current team environment | "modals.team.environment.edit" // Edit current team environment
| "modals.team.new" // Add new team | "modals.team.new" // Add new team
| "modals.team.edit" // Edit selected team | "modals.team.edit" // Edit selected team
@@ -66,6 +67,13 @@ export type HoppAction =
| "user.login" // Login to Hoppscotch | "user.login" // Login to Hoppscotch
| "user.logout" // Log out of Hoppscotch | "user.logout" // Log out of Hoppscotch
| "editor.format" // Format editor content | "editor.format" // Format editor content
| "modals.team.delete" // Delete team
| "workspace.switch" // Switch workspace
| "rest.request.open" // Open REST request
| "request.open-tab" // Open REST request
| "share.request" // Share REST request
| "tab.duplicate-tab" // Duplicate REST request
| "gql.request.open" // Open GraphQL request
/** /**
* Defines the arguments, if present for a given type that is required to be passed on * Defines the arguments, if present for a given type that is required to be passed on
@@ -86,13 +94,19 @@ type HoppActionArgsMap = {
} }
text: string | null text: string | null
} }
"modals.global.environment.update": {
variables?: Environment["variables"]
isSecret?: boolean
}
"modals.my.environment.edit": { "modals.my.environment.edit": {
envName: string envName: string
variableName?: string variableName?: string
isSecret?: boolean
} }
"modals.team.environment.edit": { "modals.team.environment.edit": {
envName: string envName: string
variableName?: string variableName?: string
isSecret?: boolean
} }
"modals.team.delete": { "modals.team.delete": {
teamId: string teamId: string
@@ -112,6 +126,7 @@ type HoppActionArgsMap = {
requestType: "gql" requestType: "gql"
request: HoppGQLRequest request: HoppGQLRequest
} }
| undefined
"request.open-tab": { "request.open-tab": {
tab: RESTOptionTabs | GQLOptionTabs tab: RESTOptionTabs | GQLOptionTabs
} }
@@ -121,7 +136,6 @@ type HoppActionArgsMap = {
"tab.duplicate-tab": { "tab.duplicate-tab": {
tabID?: string tabID?: string
} }
"gql.request.open": { "gql.request.open": {
request: HoppGQLRequest request: HoppGQLRequest
saveContext?: HoppGQLSaveContext saveContext?: HoppGQLSaveContext
@@ -132,11 +146,23 @@ type HoppActionArgsMap = {
} }
} }
type KeysWithValueUndefined<T> = {
[K in keyof T]: undefined extends T[K] ? K : never
}[keyof T]
/** /**
* HoppActions which require arguments for their invocation * HoppActions which require arguments for their invocation
*/ */
export type HoppActionWithArgs = keyof HoppActionArgsMap export type HoppActionWithArgs = keyof HoppActionArgsMap
/**
* HoppActions which optionally takes in arguments for their invocation
*/
export type HoppActionWithOptionalArgs =
| HoppActionWithNoArgs
| KeysWithValueUndefined<HoppActionArgsMap>
/** /**
* HoppActions which do not require arguments for their invocation * HoppActions which do not require arguments for their invocation
*/ */
@@ -145,27 +171,26 @@ export type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
/** /**
* Resolves the argument type for a given HoppAction * Resolves the argument type for a given HoppAction
*/ */
type ArgOfHoppAction<A extends HoppAction | HoppActionWithArgs> = type ArgOfHoppAction<A extends HoppAction> = A extends HoppActionWithArgs
A extends HoppActionWithArgs ? HoppActionArgsMap[A] : undefined ? HoppActionArgsMap[A]
: undefined
/** /**
* Resolves the action function for a given HoppAction, used by action handler function defs * Resolves the action function for a given HoppAction, used by action handler function defs
*/ */
type ActionFunc<A extends HoppAction | HoppActionWithArgs> = type ActionFunc<A extends HoppAction> = A extends HoppActionWithArgs
A extends HoppActionWithArgs ? (arg: ArgOfHoppAction<A>) => void : () => void ? (arg: ArgOfHoppAction<A>, trigger?: InvocationTriggers) => void
: (_?: undefined, trigger?: InvocationTriggers) => void
type BoundActionList = { type BoundActionList = {
// eslint-disable-next-line no-unused-vars [A in HoppAction]?: Array<ActionFunc<A>>
[A in HoppAction | HoppActionWithArgs]?: Array<ActionFunc<A>>
} }
const boundActions: BoundActionList = reactive({}) const boundActions: BoundActionList = reactive({})
export const activeActions$ = new BehaviorSubject< export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
(HoppAction | HoppActionWithArgs)[]
>([])
export function bindAction<A extends HoppAction | HoppActionWithArgs>( export function bindAction<A extends HoppAction>(
action: A, action: A,
handler: ActionFunc<A> handler: ActionFunc<A>
) { ) {
@@ -179,27 +204,33 @@ export function bindAction<A extends HoppAction | HoppActionWithArgs>(
activeActions$.next(Object.keys(boundActions) as HoppAction[]) activeActions$.next(Object.keys(boundActions) as HoppAction[])
} }
export type InvocationTriggers = "keypress" | "mouseclick"
type InvokeActionFunc = { type InvokeActionFunc = {
(action: HoppActionWithNoArgs, args?: undefined): void (
action: HoppActionWithOptionalArgs,
args?: undefined,
trigger?: InvocationTriggers
): void
<A extends HoppActionWithArgs>(action: A, args: HoppActionArgsMap[A]): void <A extends HoppActionWithArgs>(action: A, args: HoppActionArgsMap[A]): void
} }
/** /**
* Invokes a action, triggering action handlers if any registered. * Invokes an action, triggering action handlers if any registered.
* The second argument parameter is optional if your action has no args required * The second and third arguments are optional
* @param action The action to fire * @param action The action to fire
* @param args The argument passed to the action handler. Optional if action has no args required * @param args The argument passed to the action handler. Optional if action has no args required
* @param trigger Optionally supply the trigger that invoked the action (keypress/mouseclick)
*/ */
export const invokeAction: InvokeActionFunc = < export const invokeAction: InvokeActionFunc = <A extends HoppAction>(
A extends HoppAction | HoppActionWithArgs,
>(
action: A, action: A,
args: ArgOfHoppAction<A> args?: ArgOfHoppAction<A>,
trigger?: InvocationTriggers
) => { ) => {
boundActions[action]?.forEach((handler) => handler(args! as any)) boundActions[action]?.forEach((handler) => handler(args! as any, trigger))
} }
export function unbindAction<A extends HoppAction | HoppActionWithArgs>( export function unbindAction<A extends HoppAction>(
action: A, action: A,
handler: ActionFunc<A> handler: ActionFunc<A>
) { ) {
@@ -232,7 +263,7 @@ export function isActionBound(action: HoppAction): Ref<boolean> {
* @param handler The function to be called when the action is invoked * @param handler The function to be called when the action is invoked
* @param isActive A ref that indicates whether the action is active * @param isActive A ref that indicates whether the action is active
*/ */
export function defineActionHandler<A extends HoppAction | HoppActionWithArgs>( export function defineActionHandler<A extends HoppAction>(
action: A, action: A,
handler: ActionFunc<A>, handler: ActionFunc<A>,
isActive: Ref<boolean> | undefined = undefined isActive: Ref<boolean> | undefined = undefined

View File

@@ -1,7 +1,12 @@
mutation CreateTeamEnvironment($variables: String!,$teamID: ID!,$name: String!){ mutation CreateTeamEnvironment(
createTeamEnvironment( variables: $variables ,teamID: $teamID ,name: $name){ $variables: String!
$teamID: ID!
$name: String!
) {
createTeamEnvironment(variables: $variables, teamID: $teamID, name: $name) {
variables variables
name name
teamID teamID
id
} }
} }

View File

@@ -18,7 +18,7 @@ const samples = [
method: "GET", method: "GET",
name: "Untitled", name: "Untitled",
endpoint: "https://echo.hoppscotch.io/", endpoint: "https://echo.hoppscotch.io/",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
body: { body: {
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
body: rawKeyValueEntriesToString([ body: rawKeyValueEntriesToString([
@@ -149,7 +149,7 @@ const samples = [
method: "GET", method: "GET",
name: "Untitled", name: "Untitled",
endpoint: "https://google.com/", endpoint: "https://google.com/",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
body: { body: {
contentType: null, contentType: null,
body: null, body: null,
@@ -166,7 +166,7 @@ const samples = [
method: "POST", method: "POST",
name: "Untitled", name: "Untitled",
endpoint: "http://localhost:1111/hello/world/?buzz", endpoint: "http://localhost:1111/hello/world/?buzz",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
body: { body: {
contentType: "application/json", contentType: "application/json",
body: `{\n "foo": "bar"\n}`, body: `{\n "foo": "bar"\n}`,
@@ -189,7 +189,7 @@ const samples = [
method: "GET", method: "GET",
name: "Untitled", name: "Untitled",
endpoint: "https://example.com/", endpoint: "https://example.com/",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
body: { body: {
contentType: null, contentType: null,
body: null, body: null,
@@ -217,7 +217,7 @@ const samples = [
method: "POST", method: "POST",
name: "Untitled", name: "Untitled",
endpoint: "https://bing.com/", endpoint: "https://bing.com/",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
body: { body: {
contentType: "multipart/form-data", contentType: "multipart/form-data",
body: [ body: [
@@ -301,7 +301,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "http://localhost:9900/", endpoint: "http://localhost:9900/",
auth: { auth: {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
}, },
body: { body: {
@@ -345,7 +345,7 @@ const samples = [
endpoint: "https://hoppscotch.io/?io", endpoint: "https://hoppscotch.io/?io",
auth: { auth: {
authActive: true, authActive: true,
authType: "none", authType: "inherit",
}, },
body: { body: {
contentType: null, contentType: null,
@@ -380,7 +380,7 @@ const samples = [
endpoint: "https://someshadywebsite.com/questionable/path/?so", endpoint: "https://someshadywebsite.com/questionable/path/?so",
auth: { auth: {
authActive: true, authActive: true,
authType: "none", authType: "inherit",
}, },
body: { body: {
contentType: "multipart/form-data", contentType: "multipart/form-data",
@@ -441,7 +441,7 @@ const samples = [
endpoint: "http://localhost/", endpoint: "http://localhost/",
auth: { auth: {
authActive: true, authActive: true,
authType: "none", authType: "inherit",
}, },
body: { body: {
contentType: "multipart/form-data", contentType: "multipart/form-data",
@@ -473,7 +473,7 @@ const samples = [
method: "GET", method: "GET",
name: "Untitled", name: "Untitled",
endpoint: "https://hoppscotch.io/", endpoint: "https://hoppscotch.io/",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
body: { body: {
contentType: null, contentType: null,
body: null, body: null,
@@ -528,7 +528,7 @@ const samples = [
method: "GET", method: "GET",
name: "Untitled", name: "Untitled",
endpoint: "https://echo.hoppscotch.io/", endpoint: "https://echo.hoppscotch.io/",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
body: { body: {
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
body: rawKeyValueEntriesToString([ body: rawKeyValueEntriesToString([
@@ -573,7 +573,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://echo.hoppscotch.io/", endpoint: "https://echo.hoppscotch.io/",
method: "POST", method: "POST",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
headers: [ headers: [
{ {
active: true, active: true,
@@ -615,7 +615,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://muxueqz.top/skybook.html", endpoint: "https://muxueqz.top/skybook.html",
method: "GET", method: "GET",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
headers: [], headers: [],
body: { contentType: null, body: null }, body: { contentType: null, body: null },
params: [], params: [],
@@ -629,7 +629,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://echo.hoppscotch.io/", endpoint: "https://echo.hoppscotch.io/",
method: "POST", method: "POST",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
headers: [], headers: [],
body: { body: {
contentType: "multipart/form-data", contentType: "multipart/form-data",
@@ -653,7 +653,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "http://127.0.0.1/", endpoint: "http://127.0.0.1/",
method: "CUSTOMMETHOD", method: "CUSTOMMETHOD",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
headers: [], headers: [],
body: { body: {
contentType: null, contentType: null,
@@ -670,7 +670,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://echo.hoppscotch.io/", endpoint: "https://echo.hoppscotch.io/",
method: "GET", method: "GET",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
headers: [ headers: [
{ {
active: true, active: true,
@@ -693,7 +693,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://echo.hoppscotch.io/", endpoint: "https://echo.hoppscotch.io/",
method: "GET", method: "GET",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
headers: [], headers: [],
body: { body: {
contentType: null, contentType: null,
@@ -710,7 +710,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://example.org/", endpoint: "https://example.org/",
method: "HEAD", method: "HEAD",
auth: { authType: "none", authActive: true }, auth: { authType: "inherit", authActive: true },
headers: [], headers: [],
body: { body: {
contentType: null, contentType: null,
@@ -756,7 +756,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://google.com/", endpoint: "https://google.com/",
auth: { auth: {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
}, },
body: { body: {
@@ -777,7 +777,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://google.com/", endpoint: "https://google.com/",
auth: { auth: {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
}, },
body: { body: {
@@ -797,7 +797,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "http://192.168.0.24:8080/ping", endpoint: "http://192.168.0.24:8080/ping",
auth: { auth: {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
}, },
body: { body: {
@@ -817,7 +817,7 @@ const samples = [
name: "Untitled", name: "Untitled",
endpoint: "https://example.com/", endpoint: "https://example.com/",
auth: { auth: {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
}, },
body: { body: {

View File

@@ -12,14 +12,17 @@ import { parseTemplateStringE } from "@hoppscotch/data"
import { StreamSubscriberFunc } from "@composables/stream" import { StreamSubscriberFunc } from "@composables/stream"
import { import {
AggregateEnvironment, AggregateEnvironment,
aggregateEnvs$, aggregateEnvsWithSecrets$,
getAggregateEnvs, getAggregateEnvsWithSecrets,
getCurrentEnvironment,
getSelectedEnvironmentType, getSelectedEnvironmentType,
} from "~/newstore/environments" } from "~/newstore/environments"
import { invokeAction } from "~/helpers/actions" import { invokeAction } from "~/helpers/actions"
import IconUser from "~icons/lucide/user?raw" import IconUser from "~icons/lucide/user?raw"
import IconUsers from "~icons/lucide/users?raw" import IconUsers from "~icons/lucide/users?raw"
import IconEdit from "~icons/lucide/edit?raw" import IconEdit from "~icons/lucide/edit?raw"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { getService } from "~/modules/dioc"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
@@ -28,6 +31,8 @@ const HOPP_ENV_HIGHLIGHT =
const HOPP_ENV_HIGHLIGHT_FOUND = "env-found" const HOPP_ENV_HIGHLIGHT_FOUND = "env-found"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "env-not-found" const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "env-not-found"
const secretEnvironmentService = getService(SecretEnvironmentService)
const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) => const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
hoverTooltip( hoverTooltip(
(view, pos, side) => { (view, pos, side) => {
@@ -66,7 +71,27 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment" const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
const envValue = tooltipEnv?.value ?? "Not found" let envValue = "Not Found"
const currentSelectedEnvironment = getCurrentEnvironment()
const hasSecretEnv = secretEnvironmentService.hasSecretValue(
tooltipEnv?.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
tooltipEnv?.key ?? ""
)
if (!tooltipEnv?.secret && tooltipEnv?.value) envValue = tooltipEnv.value
else if (tooltipEnv?.secret && hasSecretEnv) {
envValue = "******"
} else if (tooltipEnv?.secret && !hasSecretEnv) {
envValue = "Empty"
} else if (!tooltipEnv?.sourceEnv) {
envValue = "Not Found"
} else if (!tooltipEnv?.value) {
envValue = "Empty"
}
const result = parseTemplateStringE(envValue, aggregateEnvs) const result = parseTemplateStringE(envValue, aggregateEnvs)
@@ -83,12 +108,25 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
editIcon.className = editIcon.className =
"ml-2 cursor-pointer text-accent hover:text-accentDark" "ml-2 cursor-pointer text-accent hover:text-accentDark"
editIcon.addEventListener("click", () => { editIcon.addEventListener("click", () => {
const isPersonalEnv = let invokeActionType:
envName === "Global" || selectedEnvType !== "TEAM_ENV" | "modals.my.environment.edit"
const action = isPersonalEnv ? "my" : "team" | "modals.team.environment.edit"
invokeAction(`modals.${action}.environment.edit`, { | "modals.global.environment.update" = "modals.my.environment.edit"
envName,
if (tooltipEnv?.sourceEnv === "Global") {
invokeActionType = "modals.global.environment.update"
} else if (selectedEnvType === "MY_ENV") {
invokeActionType = "modals.my.environment.edit"
} else if (selectedEnvType === "TEAM_ENV") {
invokeActionType = "modals.team.environment.edit"
} else {
invokeActionType = "modals.my.environment.edit"
}
invokeAction(invokeActionType, {
envName: tooltipEnv?.sourceEnv !== "Global" ? envName : "Global",
variableName: parsedEnvKey, variableName: parsedEnvKey,
isSecret: tooltipEnv?.secret,
}) })
}) })
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>` editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
@@ -171,11 +209,10 @@ export class HoppEnvironmentPlugin {
subscribeToStream: StreamSubscriberFunc, subscribeToStream: StreamSubscriberFunc,
private editorView: Ref<EditorView | undefined> private editorView: Ref<EditorView | undefined>
) { ) {
this.envs = getAggregateEnvs() this.envs = getAggregateEnvsWithSecrets()
subscribeToStream(aggregateEnvs$, (envs) => { subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
this.envs = envs this.envs = envs
this.editorView.value?.dispatch({ this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([ effects: this.compartment.reconfigure([
cursorTooltipField(this.envs), cursorTooltipField(this.envs),

View File

@@ -171,9 +171,6 @@ export const baseTheme = EditorView.theme({
".cm-activeLineGutter": { ".cm-activeLineGutter": {
backgroundColor: "transparent", backgroundColor: "transparent",
}, },
".cm-scroller::-webkit-scrollbar": {
display: "none",
},
".cm-foldPlaceholder": { ".cm-foldPlaceholder": {
backgroundColor: "var(--divider-light-color)", backgroundColor: "var(--divider-light-color)",
color: "var(--secondary-dark-color)", color: "var(--secondary-dark-color)",
@@ -320,9 +317,6 @@ export const inputTheme = EditorView.theme({
".cm-activeLineGutter": { ".cm-activeLineGutter": {
backgroundColor: "transparent", backgroundColor: "transparent",
}, },
".cm-scroller::-webkit-scrollbar": {
display: "none",
},
".cm-foldPlaceholder": { ".cm-foldPlaceholder": {
backgroundColor: "var(--divider-light-color)", backgroundColor: "var(--divider-light-color)",
color: "var(--secondary-dark-color)", color: "var(--secondary-dark-color)",

View File

@@ -27,7 +27,7 @@ export const getDefaultGQLRequest = (): HoppGQLRequest => ({
}`, }`,
query: DEFAULT_QUERY, query: DEFAULT_QUERY,
auth: { auth: {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
}, },
}) })

View File

@@ -12,8 +12,6 @@ const getEnvironmentJson = (
? cloneDeep(environmentObj.environment) ? cloneDeep(environmentObj.environment)
: cloneDeep(environmentObj) : cloneDeep(environmentObj)
delete newEnvironment.id
const environmentId = const environmentId =
environmentIndex || environmentIndex === 0 environmentIndex || environmentIndex === 0
? environmentIndex ? environmentIndex

View File

@@ -1,22 +1,13 @@
import * as TE from "fp-ts/TaskEither"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither"
import { entityReference } from "verzod"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { safeParseJSON } from "~/helpers/functional/json" import { safeParseJSON } from "~/helpers/functional/json"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { Environment } from "@hoppscotch/data"
import { z } from "zod" import { z } from "zod"
const hoppEnvSchema = z.object({
id: z.string().optional(),
name: z.string(),
variables: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
})
export const hoppEnvImporter = (content: string) => { export const hoppEnvImporter = (content: string) => {
const parsedContent = safeParseJSON(content, true) const parsedContent = safeParseJSON(content, true)
@@ -25,7 +16,9 @@ export const hoppEnvImporter = (content: string) => {
return TE.left(IMPORTER_INVALID_FILE_FORMAT) return TE.left(IMPORTER_INVALID_FILE_FORMAT)
} }
const validationResult = z.array(hoppEnvSchema).safeParse(parsedContent.value) const validationResult = z
.array(entityReference(Environment))
.safeParse(parsedContent.value)
if (!validationResult.success) { if (!validationResult.success) {
return TE.left(IMPORTER_INVALID_FILE_FORMAT) return TE.left(IMPORTER_INVALID_FILE_FORMAT)

View File

@@ -4,8 +4,9 @@ import * as O from "fp-ts/Option"
import { IMPORTER_INVALID_FILE_FORMAT } from "." import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { z } from "zod" import { z } from "zod"
import { Environment } from "@hoppscotch/data" import { NonSecretEnvironment } from "@hoppscotch/data"
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml" import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
import { uniqueId } from "lodash-es"
const insomniaResourcesSchema = z.object({ const insomniaResourcesSchema = z.object({
resources: z.array( resources: z.array(
@@ -56,16 +57,18 @@ export const insomniaEnvImporter = (content: string) => {
return { ...envResource, data: stringifiedData } return { ...envResource, data: stringifiedData }
}) })
const environments: Environment[] = [] const environments: NonSecretEnvironment[] = []
insomniaEnvs.forEach((insomniaEnv) => { insomniaEnvs.forEach((insomniaEnv) => {
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv) const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
if (parsedInsomniaEnv.success) { if (parsedInsomniaEnv.success) {
const environment: Environment = { const environment: NonSecretEnvironment = {
id: uniqueId(),
v: 1,
name: parsedInsomniaEnv.data.name, name: parsedInsomniaEnv.data.name,
variables: Object.entries(parsedInsomniaEnv.data.data).map( variables: Object.entries(parsedInsomniaEnv.data.data).map(
([key, value]) => ({ key, value }) ([key, value]) => ({ key, value, secret: false })
), ),
} }

View File

@@ -6,6 +6,7 @@ import { safeParseJSON } from "~/helpers/functional/json"
import { z } from "zod" import { z } from "zod"
import { Environment } from "@hoppscotch/data" import { Environment } from "@hoppscotch/data"
import { uniqueId } from "lodash-es"
const postmanEnvSchema = z.object({ const postmanEnvSchema = z.object({
name: z.string(), name: z.string(),
@@ -34,12 +35,14 @@ export const postmanEnvImporter = (content: string) => {
const postmanEnv = validationResult.data const postmanEnv = validationResult.data
const environment: Environment = { const environment: Environment = {
id: uniqueId(),
v: 1,
name: postmanEnv.name, name: postmanEnv.name,
variables: [], variables: [],
} }
postmanEnv.values.forEach(({ key, value }) => postmanEnv.values.forEach(({ key, value }) =>
environment.variables.push({ key, value }) environment.variables.push({ key, value, secret: false })
) )
return TE.right(environment) return TE.right(environment)

View File

@@ -1,5 +1,5 @@
import { onBeforeUnmount, onMounted } from "vue" import { onBeforeUnmount, onMounted } from "vue"
import { HoppActionWithNoArgs, invokeAction } from "./actions" import { HoppActionWithOptionalArgs, invokeAction } from "./actions"
import { isAppleDevice } from "./platformutils" import { isAppleDevice } from "./platformutils"
import { isDOMElement, isTypableElement } from "./utils/dom" import { isDOMElement, isTypableElement } from "./utils/dom"
@@ -40,7 +40,7 @@ type SingleCharacterShortcutKey = `${Key}`
type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
export const bindings: { export const bindings: {
[_ in ShortcutKey]?: HoppActionWithNoArgs [_ in ShortcutKey]?: HoppActionWithOptionalArgs
} = { } = {
"ctrl-enter": "request.send-cancel", "ctrl-enter": "request.send-cancel",
"ctrl-i": "request.reset", "ctrl-i": "request.reset",
@@ -96,7 +96,7 @@ function handleKeyDown(ev: KeyboardEvent) {
if (!boundAction) return if (!boundAction) return
ev.preventDefault() ev.preventDefault()
invokeAction(boundAction) invokeAction(boundAction, undefined, "keypress")
} }
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null { function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {

View File

@@ -3,41 +3,17 @@ import { PersistenceService } from "~/services/persistence"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { z } from "zod" import { z } from "zod"
import { InterceptorService } from "~/services/interceptor.service"
import { AxiosRequestConfig } from "axios"
const redirectUri = `${window.location.origin}/oauth` const redirectUri = `${window.location.origin}/oauth`
const interceptorService = getService(InterceptorService)
const persistenceService = getService(PersistenceService) const persistenceService = getService(PersistenceService)
// GENERAL HELPER FUNCTIONS // GENERAL HELPER FUNCTIONS
/**
* Makes a POST request and parse the response as JSON
*
* @param {String} url - The resource
* @param {Object} params - Configuration options
* @returns {Object}
*/
const sendPostRequest = async (url: string, params: Record<string, string>) => {
const body = Object.keys(params)
.map((key) => `${key}=${params[key]}`)
.join("&")
const options = {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body,
}
try {
const response = await fetch(url, options)
const data = await response.json()
return E.right(data)
} catch (e) {
return E.left("AUTH_TOKEN_REQUEST_FAILED")
}
}
/** /**
* Parse a query string into an object * Parse a query string into an object
* *
@@ -71,9 +47,16 @@ const getTokenConfiguration = async (endpoint: string) => {
}, },
} }
try { try {
const response = await fetch(endpoint, options) const res = await runRequestThroughInterceptor({
const config = await response.json() url: endpoint,
return E.right(config) ...options,
})
if (E.isLeft(res)) {
return E.left("OIDC_DISCOVERY_FAILED")
}
return E.right(JSON.parse(res.right))
} catch (e) { } catch (e) {
return E.left("OIDC_DISCOVERY_FAILED") return E.left("OIDC_DISCOVERY_FAILED")
} }
@@ -166,8 +149,7 @@ const tokenRequest = async ({
clientSecret, clientSecret,
scope, scope,
}: TokenRequestParams) => { }: TokenRequestParams) => {
// Check oauth configuration if (oidcDiscoveryUrl) {
if (oidcDiscoveryUrl !== "") {
const res = await getTokenConfiguration(oidcDiscoveryUrl) const res = await getTokenConfiguration(oidcDiscoveryUrl)
const OIDCConfigurationSchema = z.object({ const OIDCConfigurationSchema = z.object({
@@ -268,18 +250,24 @@ const handleOAuthRedirect = async () => {
return E.left("NO_CODE_VERIFIER" as const) return E.left("NO_CODE_VERIFIER" as const)
} }
const data = new URLSearchParams({
grant_type: "authorization_code",
code: queryParams.code,
client_id: clientID,
client_secret: clientSecret,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
})
// Exchange the authorization code for an access token // Exchange the authorization code for an access token
const tokenResponse: E.Either<string, any> = await sendPostRequest( const tokenResponse = await runRequestThroughInterceptor({
tokenEndpoint, url: tokenEndpoint,
{ data: data.toString(),
grant_type: "authorization_code", method: "POST",
code: queryParams.code, headers: {
client_id: clientID, "Content-Type": "application/x-www-form-urlencoded",
client_secret: clientSecret, },
redirect_uri: redirectUri, })
code_verifier: codeVerifier,
}
)
// Clean these up since we don't need them anymore // Clean these up since we don't need them anymore
clearPKCEState() clearPKCEState()
@@ -293,7 +281,7 @@ const handleOAuthRedirect = async () => {
}) })
const parsedTokenResponse = withAccessTokenSchema.safeParse( const parsedTokenResponse = withAccessTokenSchema.safeParse(
tokenResponse.right JSON.parse(tokenResponse.right)
) )
return parsedTokenResponse.success return parsedTokenResponse.success
@@ -309,4 +297,20 @@ const clearPKCEState = () => {
persistenceService.removeLocalConfig("client_secret") persistenceService.removeLocalConfig("client_secret")
} }
async function runRequestThroughInterceptor(config: AxiosRequestConfig) {
const res = await interceptorService.runRequest(config).response
if (E.isLeft(res)) {
return E.left("REQUEST_FAILED")
}
// convert ArrayBuffer to string
if (!(res.right.data instanceof ArrayBuffer)) {
return E.left("REQUEST_FAILED")
}
const data = new TextDecoder().decode(res.right.data).replace(/\0+$/, "")
return E.right(data)
}
export { tokenRequest, handleOAuthRedirect } export { tokenRequest, handleOAuthRedirect }

View File

@@ -8,11 +8,71 @@ import {
getGlobalVariables, getGlobalVariables,
} from "~/newstore/environments" } from "~/newstore/environments"
import { TestResult } from "@hoppscotch/js-sandbox" import { TestResult } from "@hoppscotch/js-sandbox"
import { getService } from "~/modules/dioc"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
export const getCombinedEnvVariables = () => ({ const secretEnvironmentService = getService(SecretEnvironmentService)
global: cloneDeep(getGlobalVariables()),
selected: cloneDeep(getCurrentEnvironment().variables), const unsecretEnvironments = (
}) global: Environment["variables"],
selected: Environment
) => {
const resolvedGlobalWithSecrets = global.map((globalVar, index) => {
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
"Global",
index
)
if (secretVar) {
return {
...globalVar,
value: secretVar.value,
}
} else if (!("value" in globalVar) || !globalVar.value) {
return {
...globalVar,
value: "",
}
}
return globalVar
})
const resolvedSelectedWithSecrets = selected.variables.map(
(selectedVar, index) => {
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
selected.id,
index
)
if (secretVar) {
return {
...selectedVar,
value: secretVar.value,
}
} else if (!("value" in selectedVar) || !selectedVar.value) {
return {
...selectedVar,
value: "",
}
}
return selectedVar
}
)
return {
global: resolvedGlobalWithSecrets,
selected: resolvedSelectedWithSecrets,
}
}
export const getCombinedEnvVariables = () => {
const reformedVars = unsecretEnvironments(
getGlobalVariables(),
getCurrentEnvironment()
)
return {
global: cloneDeep(reformedVars.global),
selected: cloneDeep(reformedVars.selected),
}
}
export const getFinalEnvsFromPreRequest = ( export const getFinalEnvsFromPreRequest = (
script: string, script: string,

View File

@@ -8,7 +8,7 @@ export const getDefaultRESTRequest = (): HoppRESTRequest => ({
headers: [], headers: [],
method: "GET", method: "GET",
auth: { auth: {
authType: "none", authType: "inherit",
authActive: true, authActive: true,
}, },
preRequestScript: "", preRequestScript: "",

View File

@@ -118,6 +118,8 @@ export default class TeamEnvironmentAdapter {
id: x.id, id: x.id,
teamID: x.teamID, teamID: x.teamID,
environment: { environment: {
v: 1,
id: x.id,
name: x.name, name: x.name,
variables: JSON.parse(x.variables), variables: JSON.parse(x.variables),
}, },
@@ -196,6 +198,8 @@ export default class TeamEnvironmentAdapter {
id: x.id, id: x.id,
teamID: x.teamID, teamID: x.teamID,
environment: { environment: {
v: 1,
id: x.id,
name: x.name, name: x.name,
variables: JSON.parse(x.variables), variables: JSON.parse(x.variables),
}, },
@@ -249,6 +253,8 @@ export default class TeamEnvironmentAdapter {
id: x.id, id: x.id,
teamID: x.teamID, teamID: x.teamID,
environment: { environment: {
v: 1,
id: x.id,
name: x.name, name: x.name,
variables: JSON.parse(x.variables), variables: JSON.parse(x.variables),
}, },

View File

@@ -1256,5 +1256,11 @@
"!type": "fn(value: ?) -> bool" "!type": "fn(value: ?) -> bool"
} }
} }
},
"btoa": {
"!type": "fn(data: string) -> string"
},
"atob": {
"!type": "fn(data: string) -> string"
} }
} }

View File

@@ -45,7 +45,8 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
export const getComputedAuthHeaders = ( export const getComputedAuthHeaders = (
envVars: Environment["variables"], envVars: Environment["variables"],
req?: HoppRESTRequest, req?: HoppRESTRequest,
auth?: HoppRESTRequest["auth"] auth?: HoppRESTRequest["auth"],
parse = true
) => { ) => {
const request = auth ? { auth: auth ?? { authActive: false } } : req const request = auth ? { auth: auth ?? { authActive: false } } : req
// If Authorization header is also being user-defined, that takes priority // If Authorization header is also being user-defined, that takes priority
@@ -60,8 +61,12 @@ export const getComputedAuthHeaders = (
// TODO: Support a better b64 implementation than btoa ? // TODO: Support a better b64 implementation than btoa ?
if (request.auth.authType === "basic") { if (request.auth.authType === "basic") {
const username = parseTemplateString(request.auth.username, envVars) const username = parse
const password = parseTemplateString(request.auth.password, envVars) ? parseTemplateString(request.auth.username, envVars)
: request.auth.username
const password = parse
? parseTemplateString(request.auth.password, envVars)
: request.auth.password
headers.push({ headers.push({
active: true, active: true,
@@ -75,7 +80,11 @@ export const getComputedAuthHeaders = (
headers.push({ headers.push({
active: true, active: true,
key: "Authorization", key: "Authorization",
value: `Bearer ${parseTemplateString(request.auth.token, envVars)}`, value: `Bearer ${
parse
? parseTemplateString(request.auth.token, envVars)
: request.auth.token
}`,
}) })
} else if (request.auth.authType === "api-key") { } else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth const { key, addTo } = request.auth
@@ -83,7 +92,9 @@ export const getComputedAuthHeaders = (
headers.push({ headers.push({
active: true, active: true,
key: parseTemplateString(key, envVars), key: parseTemplateString(key, envVars),
value: parseTemplateString(request.auth.value ?? "", envVars), value: parse
? parseTemplateString(request.auth.value ?? "", envVars)
: request.auth.value ?? "",
}) })
} }
} }
@@ -133,10 +144,11 @@ export type ComputedHeader = {
*/ */
export const getComputedHeaders = ( export const getComputedHeaders = (
req: HoppRESTRequest, req: HoppRESTRequest,
envVars: Environment["variables"] envVars: Environment["variables"],
parse = true
): ComputedHeader[] => { ): ComputedHeader[] => {
return [ return [
...getComputedAuthHeaders(envVars, req).map((header) => ({ ...getComputedAuthHeaders(envVars, req, undefined, parse).map((header) => ({
source: "auth" as const, source: "auth" as const,
header, header,
})), })),

View File

@@ -69,13 +69,15 @@ import "splitpanes/dist/splitpanes.css"
import { computed, onBeforeMount, onMounted, ref, watch } from "vue" import { computed, onBeforeMount, onMounted, ref, watch } from "vue"
import { RouterView, useRouter } from "vue-router" import { RouterView, useRouter } from "vue-router"
import { defineActionHandler } from "~/helpers/actions" import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { InvocationTriggers, defineActionHandler } from "~/helpers/actions"
import { hookKeybindingsListener } from "~/helpers/keybindings" import { hookKeybindingsListener } from "~/helpers/keybindings"
import { applySetting } from "~/newstore/settings" import { applySetting } from "~/newstore/settings"
import { useToast } from "~/composables/toast"
import { useI18n } from "~/composables/i18n"
import { platform } from "~/platform" import { platform } from "~/platform"
import { HoppSpotlightSessionEventData } from "~/platform/analytics"
import { PersistenceService } from "~/services/persistence" import { PersistenceService } from "~/services/persistence"
import { SpotlightService } from "~/services/spotlight"
const router = useRouter() const router = useRouter()
@@ -93,6 +95,7 @@ const toast = useToast()
const t = useI18n() const t = useI18n()
const persistenceService = useService(PersistenceService) const persistenceService = useService(PersistenceService)
const spotlightService = useService(SpotlightService)
onBeforeMount(() => { onBeforeMount(() => {
if (!mdAndLarger.value) { if (!mdAndLarger.value) {
@@ -144,7 +147,18 @@ const spacerClass = computed(() =>
expandNavigation.value ? "spacer-small" : "spacer-expand" expandNavigation.value ? "spacer-small" : "spacer-expand"
) )
defineActionHandler("modals.search.toggle", () => { defineActionHandler("modals.search.toggle", (_, trigger) => {
const triggerMethodMap: Record<
InvocationTriggers,
HoppSpotlightSessionEventData["method"]
> = {
keypress: "keyboard-shortcut",
mouseclick: "click-spotlight-bar",
}
spotlightService.setAnalyticsData({
method: triggerMethodMap[trigger as InvocationTriggers],
})
showSearch.value = !showSearch.value showSearch.value = !showSearch.value
}) })

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