Compare commits
55 Commits
feat/admin
...
pr/JoelJac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7876c5cc58 | ||
|
|
e09fb32219 | ||
|
|
b5cf2fa1b4 | ||
|
|
0e2dd400e4 | ||
|
|
f99bfda88b | ||
|
|
446d29f519 | ||
|
|
d0032d6f50 | ||
|
|
fbf5d5ba71 | ||
|
|
14506cd92a | ||
|
|
f38e2b6884 | ||
|
|
ac11a804bd | ||
|
|
25d8c28e4f | ||
|
|
ae9e32b427 | ||
|
|
041eeab4d2 | ||
|
|
6d1e8c4eeb | ||
|
|
08992089f7 | ||
|
|
cc45ff595b | ||
|
|
ba58e25278 | ||
|
|
e261f75cc8 | ||
|
|
55293a0382 | ||
|
|
f7deff5448 | ||
|
|
8877dae2dd | ||
|
|
162434fad4 | ||
|
|
5095c2b76f | ||
|
|
70c4ef5699 | ||
|
|
5959f422a0 | ||
|
|
5c59e55e53 | ||
|
|
55a94bdccc | ||
|
|
faab1d20fd | ||
|
|
bd406616ec | ||
|
|
6827e97ec5 | ||
|
|
10d2048975 | ||
|
|
291f18591e | ||
|
|
342532c9b1 | ||
|
|
cf039c482a | ||
|
|
ded2725116 | ||
|
|
4bd54b12cd | ||
|
|
ed6e9b6954 | ||
|
|
dfdd44b4ed | ||
|
|
fc34871dae | ||
|
|
45b532747e | ||
|
|
de4635df23 | ||
|
|
41bad1f3dc | ||
|
|
ecca3d2032 | ||
|
|
47226be6d0 | ||
|
|
6a0e73fdec | ||
|
|
672ee69b2c | ||
|
|
c0fae79678 | ||
|
|
5bcc38e36b | ||
|
|
00862eb192 | ||
|
|
16803acb26 | ||
|
|
3911c9cd1f | ||
|
|
0028f6e878 | ||
|
|
0ba33ec187 | ||
|
|
d7cdeb796a |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.12.3",
|
||||
"version": "2023.12.6",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -34,12 +34,14 @@
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-express": "^10.2.6",
|
||||
"@nestjs/schedule": "^4.0.1",
|
||||
"@nestjs/throttler": "^5.0.0",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"argon2": "^0.30.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cron": "^3.1.6",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.3",
|
||||
"fp-ts": "^2.13.1",
|
||||
@@ -57,6 +59,7 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-microsoft": "^1.0.0",
|
||||
"posthog-node": "^3.6.3",
|
||||
"prisma": "^5.8.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
InfraConfigArgs,
|
||||
} from 'src/infra-config/input-args';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
import { ServiceStatus } from 'src/infra-config/helper';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Infra)
|
||||
@@ -310,6 +311,25 @@ export class InfraResolver {
|
||||
return updatedRes.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Enable or disable analytics collection',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async toggleAnalyticsCollection(
|
||||
@Args({
|
||||
name: 'status',
|
||||
type: () => ServiceStatus,
|
||||
description: 'Toggle analytics collection',
|
||||
})
|
||||
analyticsCollectionStatus: ServiceStatus,
|
||||
) {
|
||||
const res = await this.infraConfigService.toggleAnalyticsCollection(
|
||||
analyticsCollectionStatus,
|
||||
);
|
||||
if (E.isLeft(res)) throwErr(res.left);
|
||||
return res.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Reset Infra Configs with default values (.env)',
|
||||
})
|
||||
|
||||
@@ -24,6 +24,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { InfraConfigModule } from './infra-config/infra-config.module';
|
||||
import { loadInfraConfiguration } from './infra-config/helper';
|
||||
import { MailerModule } from './mailer/mailer.module';
|
||||
import { PosthogModule } from './posthog/posthog.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -96,6 +98,8 @@ import { MailerModule } from './mailer/mailer.module';
|
||||
UserCollectionModule,
|
||||
ShortcodeModule,
|
||||
InfraConfigModule,
|
||||
PosthogModule,
|
||||
ScheduleModule.forRoot(),
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -711,3 +711,9 @@ export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
||||
*/
|
||||
export const DATABASE_TABLE_NOT_EXIST =
|
||||
'Database migration not found. Please check the documentation for assistance: https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build#running-migrations';
|
||||
|
||||
/**
|
||||
* PostHog client is not initialized
|
||||
* (InfraConfigService)
|
||||
*/
|
||||
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export enum ServiceStatus {
|
||||
ENABLE = 'ENABLE',
|
||||
@@ -104,3 +105,12 @@ export function getConfiguredSSOProviders() {
|
||||
|
||||
return configuredAuthProviders.join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hashed valued for analytics
|
||||
* @returns Generated hashed value
|
||||
*/
|
||||
export function generateAnalyticsUserId() {
|
||||
const hashedUserID = randomBytes(20).toString('hex');
|
||||
return hashedUserID;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@ import {
|
||||
} from 'src/errors';
|
||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ServiceStatus, getConfiguredSSOProviders, stopApp } from './helper';
|
||||
import {
|
||||
ServiceStatus,
|
||||
generateAnalyticsUserId,
|
||||
getConfiguredSSOProviders,
|
||||
stopApp,
|
||||
} from './helper';
|
||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
|
||||
@@ -75,6 +80,14 @@ export class InfraConfigService implements OnModuleInit {
|
||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||
value: getConfiguredSSOProviders(),
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||
value: false.toString(),
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
||||
value: generateAnalyticsUserId(),
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||
value: (await this.prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
||||
@@ -231,6 +244,22 @@ export class InfraConfigService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or Disable Analytics Collection
|
||||
*
|
||||
* @param status Status to enable or disable
|
||||
* @returns Boolean of status of analytics collection
|
||||
*/
|
||||
async toggleAnalyticsCollection(status: ServiceStatus) {
|
||||
const isUpdated = await this.update(
|
||||
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||
status === ServiceStatus.ENABLE ? 'true' : 'false',
|
||||
);
|
||||
|
||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||
return E.right(isUpdated.right.value === 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or Disable SSO for login/signup
|
||||
* @param provider Auth Provider to enable or disable
|
||||
|
||||
@@ -25,7 +25,7 @@ export class MailerService {
|
||||
): string {
|
||||
switch (mailDesc.template) {
|
||||
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':
|
||||
return 'Sign in to Hoppscotch';
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a.nohighlight {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
@@ -458,7 +464,7 @@
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<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 -->
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
@@ -484,7 +490,7 @@
|
||||
Welcome aboard, <br />
|
||||
Your friends at Hoppscotch
|
||||
</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 -->
|
||||
<table class="body-sub">
|
||||
<tr>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
|
||||
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
body {
|
||||
width: 100% !important;
|
||||
@@ -22,19 +22,25 @@
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
|
||||
a.nohighlight {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
@@ -47,13 +53,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -61,7 +67,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -69,7 +75,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -77,12 +83,12 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
@@ -91,25 +97,25 @@
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
@@ -124,7 +130,7 @@
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
@@ -132,7 +138,7 @@
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
@@ -140,7 +146,7 @@
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
@@ -148,21 +154,21 @@
|
||||
}
|
||||
}
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -171,31 +177,31 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -206,33 +212,33 @@
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -241,7 +247,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -250,50 +256,50 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -303,7 +309,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -313,16 +319,16 @@
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
@@ -331,7 +337,7 @@
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -340,7 +346,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -350,7 +356,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -360,11 +366,11 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
@@ -374,25 +380,25 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.content-cell {
|
||||
padding: 45px;
|
||||
}
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.email-body,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PosthogService } from './posthog.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [PosthogService],
|
||||
})
|
||||
export class PosthogModule {}
|
||||
58
packages/hoppscotch-backend/src/posthog/posthog.service.ts
Normal file
58
packages/hoppscotch-backend/src/posthog/posthog.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PostHog } from 'posthog-node';
|
||||
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { CronJob } from 'cron';
|
||||
import { POSTHOG_CLIENT_NOT_INITIALIZED } from 'src/errors';
|
||||
import { throwErr } from 'src/utils';
|
||||
@Injectable()
|
||||
export class PosthogService {
|
||||
private postHogClient: PostHog;
|
||||
private POSTHOG_API_KEY = 'phc_9CipPajQC22mSkk2wxe2TXsUA0Ysyupe8dt5KQQELqx';
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
if (this.configService.get('INFRA.ALLOW_ANALYTICS_COLLECTION') === 'true') {
|
||||
console.log('Initializing PostHog');
|
||||
this.postHogClient = new PostHog(this.POSTHOG_API_KEY, {
|
||||
host: 'https://eu.posthog.com',
|
||||
});
|
||||
|
||||
// Schedule the cron job only if analytics collection is allowed
|
||||
this.scheduleCronJob();
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleCronJob() {
|
||||
const job = new CronJob(CronExpression.EVERY_WEEK, async () => {
|
||||
await this.capture();
|
||||
});
|
||||
|
||||
this.schedulerRegistry.addCronJob('captureAnalytics', job);
|
||||
job.start();
|
||||
}
|
||||
|
||||
async capture() {
|
||||
if (!this.postHogClient) {
|
||||
throwErr(POSTHOG_CLIENT_NOT_INITIALIZED);
|
||||
}
|
||||
|
||||
this.postHogClient.capture({
|
||||
distinctId: this.configService.get('INFRA.ANALYTICS_USER_ID'),
|
||||
event: 'sh_instance',
|
||||
properties: {
|
||||
type: 'COMMUNITY',
|
||||
total_user_count: await this.prismaService.user.count(),
|
||||
total_workspace_count: await this.prismaService.team.count(),
|
||||
version: this.configService.get('npm_package_version'),
|
||||
},
|
||||
});
|
||||
console.log('Sent event to PostHog');
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ export enum InfraConfigEnum {
|
||||
|
||||
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
||||
|
||||
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
||||
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||
}
|
||||
|
||||
@@ -29,5 +31,6 @@ export enum InfraConfigEnumForClient {
|
||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||
|
||||
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||
}
|
||||
|
||||
@@ -58,9 +58,13 @@
|
||||
"@types/qs": "^6.9.11",
|
||||
"fp-ts": "^2.16.2",
|
||||
"jest": "^29.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "^3.2.4",
|
||||
"qs": "^6.11.2",
|
||||
"ts-jest": "^29.1.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.3.3",
|
||||
"verzod": "^0.2.2",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,138 +3,247 @@ import { ExecException } from "child_process";
|
||||
import { HoppErrorCode } from "../../types/errors";
|
||||
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
|
||||
describe("Test 'hopp test <file>' command:", () => {
|
||||
test("No collection file path provided.", async () => {
|
||||
const args = "test";
|
||||
const { stderr } = await runCLI(args);
|
||||
describe("Test `hopp test <file>` command:", () => {
|
||||
describe("Argument parsing", () => {
|
||||
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
|
||||
const args = "test";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not found.", async () => {
|
||||
const args = "test notfound.json";
|
||||
const { stderr } = await runCLI(args);
|
||||
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
|
||||
const args = "invalid-arg";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
})
|
||||
|
||||
test("Collection file is invalid JSON.", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"malformed-collection.json"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
describe("Supplied collection export file validations", () => {
|
||||
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
|
||||
const args = "test notfound.json";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
|
||||
const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
test("Invalid arguement.", async () => {
|
||||
const args = "invalid-arg";
|
||||
const { stderr } = await runCLI(args);
|
||||
test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
|
||||
const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
test("Collection file not JSON type.", async () => {
|
||||
const args = `test ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
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-coll.txt", "collection")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
|
||||
});
|
||||
|
||||
test("Some errors occured (exit code 1).", async () => {
|
||||
const args = `test ${getTestJsonFilePath("fails.json")}`;
|
||||
const { error } = await runCLI(args);
|
||||
test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
|
||||
const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toMatchObject(<ExecException>{
|
||||
code: 1,
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toMatchObject(<ExecException>{
|
||||
code: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("No errors occured (exit code 0).", async () => {
|
||||
const args = `test ${getTestJsonFilePath("passes.json")}`;
|
||||
test("Successfully processes a supplied collection export file of the expected format", async () => {
|
||||
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Supports inheriting headers and authorization set at the root collection", async () => {
|
||||
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`;
|
||||
test("Successfully inherits headers and authorization set at the root collection", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"collection-level-headers-auth-coll.json", "collection"
|
||||
)}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
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:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
describe("Supplied environment export file validations", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
|
||||
test("No env file path provided.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env`;
|
||||
const { stderr } = await runCLI(args);
|
||||
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
const out = getErrorCode(stderr);
|
||||
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 () => {
|
||||
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
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");
|
||||
test("Successfully resolves values from the supplied environment export file", 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} --env ${ENV_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Correctly resolves environment variables referenced in the request body", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json");
|
||||
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json");
|
||||
test("Successfully resolves environment variables referenced in the request body", async () => {
|
||||
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
|
||||
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
|
||||
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
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:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
|
||||
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 { stderr } = await runCLI(args);
|
||||
|
||||
@@ -142,7 +251,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
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 { stderr } = await runCLI(args);
|
||||
|
||||
@@ -150,10 +259,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
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 { error } = await runCLI(args);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": 123,
|
||||
"v": "1",
|
||||
"name": "secret-envs",
|
||||
"values": [
|
||||
{
|
||||
"key": "secretVar",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "regularVar",
|
||||
"secret": false,
|
||||
"value": "regular-variable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"v": 0,
|
||||
"name": "Response body sample",
|
||||
"variables": [
|
||||
{
|
||||
@@ -34,4 +35,4 @@
|
||||
"value": "<<salutation>> <<fullName>>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import { resolve } from "path";
|
||||
|
||||
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 command = `node ${CLI_PATH} ${args}`
|
||||
|
||||
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];
|
||||
};
|
||||
|
||||
export const getTestJsonFilePath = (file: string) => {
|
||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${file}`);
|
||||
export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => {
|
||||
const kindDir = {
|
||||
collection: "collections",
|
||||
environment: "environments",
|
||||
}[kind];
|
||||
|
||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`);
|
||||
return filePath;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface RequestStack {
|
||||
*/
|
||||
export interface RequestConfig extends AxiosRequestConfig {
|
||||
supported: boolean;
|
||||
displayUrl?: string
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
effectiveFinalURL: string;
|
||||
effectiveFinalDisplayURL?: string;
|
||||
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalBody: FormData | string | null;
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
import { Environment } from "@hoppscotch/data";
|
||||
import { entityReference } from "verzod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { error } from "../../types/errors";
|
||||
import {
|
||||
HoppEnvs,
|
||||
HoppEnvPair,
|
||||
HoppEnvKeyPairObject,
|
||||
HoppEnvExportObject,
|
||||
HoppBulkEnvExportObject,
|
||||
HoppEnvPair,
|
||||
HoppEnvs
|
||||
} from "../../types/request";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @returns For successful parsing we get HoppEnvs 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
|
||||
* @returns For successful parsing we get HoppEnvs object
|
||||
*/
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path);
|
||||
const envPairs: Array<HoppEnvPair> = [];
|
||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
||||
const HoppBulkEnvExportObjectResult =
|
||||
HoppBulkEnvExportObject.safeParse(contents);
|
||||
const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
|
||||
|
||||
// CLI doesnt support bulk environments export.
|
||||
// Hence we check for this case and throw an error if it matches the format.
|
||||
// The legacy key-value pair format that is still supported
|
||||
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) {
|
||||
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
// Checks if the environment file is of the correct format.
|
||||
// If it doesnt match either of them, we throw an error.
|
||||
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
||||
// Checks if the environment file is of the correct format
|
||||
// If it doesnt match either of them, we throw an error
|
||||
if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
|
||||
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)) {
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
} else if (HoppEnvExportObjectResult.success) {
|
||||
envPairs.push(...HoppEnvExportObjectResult.data.variables);
|
||||
} else if (HoppEnvExportObjectResult.type === "ok") {
|
||||
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
||||
}
|
||||
|
||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||
|
||||
@@ -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 { HoppCLIError } from "./errors";
|
||||
import { z } from "zod";
|
||||
|
||||
export type FormDataEntry = {
|
||||
key: string;
|
||||
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());
|
||||
|
||||
// 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 = {
|
||||
global: HoppEnvPair[];
|
||||
selected: HoppEnvPair[];
|
||||
|
||||
@@ -176,7 +176,7 @@ export const printRequestRunner = {
|
||||
*/
|
||||
start: (requestConfig: RequestConfig) => {
|
||||
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
||||
const ENDPOINT = requestConfig.url;
|
||||
const ENDPOINT = requestConfig.displayUrl || requestConfig.url;
|
||||
|
||||
process.stdout.write(`${METHOD} ${ENDPOINT}`);
|
||||
},
|
||||
|
||||
@@ -36,7 +36,10 @@ import { toFormData } from "./mutators";
|
||||
export const preRequestScriptRunner = (
|
||||
request: HoppRESTRequest,
|
||||
envs: HoppEnvs
|
||||
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
|
||||
): TE.TaskEither<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> =>
|
||||
pipe(
|
||||
TE.of(request),
|
||||
TE.chain(({ preRequestScript }) =>
|
||||
@@ -68,7 +71,10 @@ export const preRequestScriptRunner = (
|
||||
export function getEffectiveRESTRequest(
|
||||
request: HoppRESTRequest,
|
||||
environment: Environment
|
||||
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
|
||||
): E.Either<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> {
|
||||
const envVariables = environment.variables;
|
||||
|
||||
// Parsing final headers with applied ENVs.
|
||||
@@ -162,12 +168,30 @@ export function getEffectiveRESTRequest(
|
||||
}
|
||||
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({
|
||||
...request,
|
||||
effectiveFinalURL,
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
effectiveRequest: {
|
||||
...request,
|
||||
effectiveFinalURL,
|
||||
effectiveFinalDisplayURL,
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
},
|
||||
updatedEnvs: { global: [], selected: envVariables },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import axios, { Method } from "axios";
|
||||
import * as A from "fp-ts/Array";
|
||||
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
|
||||
|
||||
/**
|
||||
* 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
|
||||
* perform HTTP request.
|
||||
@@ -38,6 +70,7 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
|
||||
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
||||
const config: RequestConfig = {
|
||||
supported: true,
|
||||
displayUrl: req.effectiveFinalDisplayURL
|
||||
};
|
||||
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
||||
const reqParams = finalParams(req);
|
||||
@@ -221,9 +254,13 @@ export const processRequest =
|
||||
effectiveFinalParams: [],
|
||||
effectiveFinalURL: "",
|
||||
};
|
||||
let updatedEnvs = <HoppEnvs>{};
|
||||
|
||||
// Fetch values for secret environment variables from system environment
|
||||
const processedEnvs = processEnvs(envs)
|
||||
|
||||
// Executing pre-request-script
|
||||
const preRequestRes = await preRequestScriptRunner(request, envs)();
|
||||
const preRequestRes = await preRequestScriptRunner(request, processedEnvs)();
|
||||
if (E.isLeft(preRequestRes)) {
|
||||
printPreRequestRunner.fail();
|
||||
|
||||
@@ -231,8 +268,8 @@ export const processRequest =
|
||||
report.errors.push(preRequestRes.left);
|
||||
report.result = report.result && false;
|
||||
} else {
|
||||
// Updating effective-request
|
||||
effectiveRequest = preRequestRes.right;
|
||||
// Updating effective-request and consuming updated envs after pre-request script execution
|
||||
({ effectiveRequest, updatedEnvs } = preRequestRes.right);
|
||||
}
|
||||
|
||||
// Creating request-config for request-runner.
|
||||
@@ -270,7 +307,7 @@ export const processRequest =
|
||||
const testScriptParams = getTestScriptParams(
|
||||
_requestRunnerRes,
|
||||
request,
|
||||
envs
|
||||
updatedEnvs
|
||||
);
|
||||
|
||||
// Executing test-runner.
|
||||
|
||||
@@ -429,6 +429,11 @@ pre.ace_editor {
|
||||
}
|
||||
}
|
||||
|
||||
.splitpanes__pane {
|
||||
@apply will-change-auto;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply before:absolute;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"go_back": "Go back",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"hide_secret": "Hide secret",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
"less": "Less",
|
||||
@@ -43,6 +44,7 @@
|
||||
"search": "Search",
|
||||
"send": "Send",
|
||||
"share": "Share",
|
||||
"show_secret": "Show secret",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
@@ -238,6 +240,7 @@
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protocols are empty",
|
||||
"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_logout": "Login to view your shared requests or create a new one",
|
||||
"subscription": "Subscriptions are empty",
|
||||
@@ -269,6 +272,8 @@
|
||||
"quick_peek": "Environment Quick Peek",
|
||||
"replace_with_variable": "Replace with variable",
|
||||
"scope": "Scope",
|
||||
"secrets": "Secrets",
|
||||
"secret_value": "Secret value",
|
||||
"select": "Select environment",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
@@ -277,6 +282,7 @@
|
||||
"updated": "Environment updated",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"variables":"Variables",
|
||||
"variable_list": "Variable List"
|
||||
},
|
||||
"error": {
|
||||
@@ -413,6 +419,8 @@
|
||||
"description": "Inspect possible errors",
|
||||
"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."
|
||||
},
|
||||
"header": {
|
||||
@@ -889,6 +897,7 @@
|
||||
"query": "Query",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"share_tab_request": "Share tab request",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Tests",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "2023.12.3",
|
||||
"version": "2023.12.6",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"test": "vitest --run",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||
<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"
|
||||
@click="invokeAction('modals.search.toggle')"
|
||||
@click="invokeAction('modals.search.toggle', undefined, 'mouseclick')"
|
||||
>
|
||||
<span class="inline-flex flex-1 items-center">
|
||||
<icon-lucide-search class="svg-icons mr-2" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="show"
|
||||
styles="sm:max-w-lg"
|
||||
full-width
|
||||
@close="emit('hide-modal')"
|
||||
@close="closeSpotlightModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col border-b border-divider transition">
|
||||
@@ -86,35 +86,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { useService } from "dioc/vue"
|
||||
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 {
|
||||
SpotlightService,
|
||||
SpotlightSearchState,
|
||||
SpotlightSearcherResult,
|
||||
SpotlightService,
|
||||
} 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 { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
|
||||
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
||||
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
|
||||
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
||||
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
|
||||
import {
|
||||
EnvironmentsSpotlightSearcherService,
|
||||
SwitchEnvSpotlightSearcherService,
|
||||
} from "~/services/spotlight/searchers/environment.searcher"
|
||||
import { 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 {
|
||||
SwitchWorkspaceSpotlightSearcherService,
|
||||
WorkspaceSpotlightSearcherService,
|
||||
} from "~/services/spotlight/searchers/workspace.searcher"
|
||||
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -290,4 +291,17 @@ function newUseArrowKeysForNavigation() {
|
||||
|
||||
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>
|
||||
|
||||
@@ -614,8 +614,8 @@ const addNewRootCollection = (name: string) => {
|
||||
requests: [],
|
||||
headers: [],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: false,
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'cookie')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
@@ -102,6 +102,8 @@ import {
|
||||
useCopyResponse,
|
||||
useDownloadResponse,
|
||||
} from "~/composables/lens-actions"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
// TODO: Build Managed Mode!
|
||||
|
||||
@@ -122,7 +124,7 @@ const toast = useToast()
|
||||
|
||||
const cookieEditor = ref<HTMLElement>()
|
||||
const rawCookieString = ref("")
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "cookie")
|
||||
|
||||
useCodemirror(
|
||||
cookieEditor,
|
||||
@@ -131,7 +133,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
||||
t("environment.value")
|
||||
}}</label>
|
||||
<input
|
||||
<SmartEnvInput
|
||||
v-model="editingValue"
|
||||
type="text"
|
||||
class="input"
|
||||
@@ -154,12 +154,14 @@ const addEnvironment = async () => {
|
||||
addGlobalEnvVariable({
|
||||
key: editingName.value,
|
||||
value: editingValue.value,
|
||||
secret: false,
|
||||
})
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
} else if (scope.value.type === "my-environment") {
|
||||
addEnvironmentVariable(scope.value.index, {
|
||||
key: editingName.value,
|
||||
value: editingValue.value,
|
||||
secret: false,
|
||||
})
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { Environment, NonSecretEnvironment } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { ref } from "vue"
|
||||
|
||||
@@ -340,13 +340,13 @@ const showImportFailedError = () => {
|
||||
|
||||
const handleImportToStore = async (
|
||||
environments: Environment[],
|
||||
globalEnv?: Environment
|
||||
globalEnv?: NonSecretEnvironment
|
||||
) => {
|
||||
// if there's a global env, add them to the store
|
||||
if (globalEnv) {
|
||||
globalEnv.variables.forEach(({ key, value }) => {
|
||||
addGlobalEnvVariable({ key, value })
|
||||
})
|
||||
globalEnv.variables.forEach(({ key, value, secret }) =>
|
||||
addGlobalEnvVariable({ key, value, secret })
|
||||
)
|
||||
}
|
||||
|
||||
if (props.environmentType === "MY_ENV") {
|
||||
|
||||
@@ -210,7 +210,10 @@
|
||||
{{ variable.key }}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
||||
@@ -265,7 +268,10 @@
|
||||
{{ variable.key }}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
<div
|
||||
@@ -479,15 +485,20 @@ const selectedEnv = computed(() => {
|
||||
type: "MY_ENV",
|
||||
index: props.modelValue.index,
|
||||
name: props.modelValue.environment?.name,
|
||||
variables: props.modelValue.environment?.variables,
|
||||
}
|
||||
} else if (props.modelValue?.type === "team-environment") {
|
||||
return {
|
||||
type: "TEAM_ENV",
|
||||
name: props.modelValue.environment.environment.name,
|
||||
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") {
|
||||
const environment =
|
||||
@@ -582,9 +593,7 @@ const environmentVariables = computed(() => {
|
||||
})
|
||||
|
||||
const editGlobalEnv = () => {
|
||||
invokeAction("modals.my.environment.edit", {
|
||||
envName: "Global",
|
||||
})
|
||||
invokeAction("modals.global.environment.update", {})
|
||||
}
|
||||
|
||||
const editEnv = () => {
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
:action="action"
|
||||
:editing-environment-index="editingEnvironmentIndex"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:env-vars="envVars"
|
||||
:is-secret-option-selected="secretOptionSelected"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsAdd
|
||||
@@ -37,7 +39,7 @@
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="showConfirmRemoveEnvModal"
|
||||
:title="t('confirm.remove_team')"
|
||||
:title="`${t('confirm.remove_environment')}`"
|
||||
@hide-modal="showConfirmRemoveEnvModal = false"
|
||||
@resolve="removeSelectedEnvironment()"
|
||||
/>
|
||||
@@ -67,6 +69,7 @@ import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironme
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -88,6 +91,8 @@ const environmentType = ref<EnvironmentsChooseType>({
|
||||
const globalEnv = useReadonlyStream(globalEnv$, [])
|
||||
|
||||
const globalEnvironment = computed(() => ({
|
||||
v: 1 as const,
|
||||
id: "Global",
|
||||
name: "Global",
|
||||
variables: globalEnv.value,
|
||||
}))
|
||||
@@ -186,6 +191,7 @@ const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironmentIndex = ref<"Global" | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
const editingVariableValue = ref("")
|
||||
const secretOptionSelected = ref(false)
|
||||
|
||||
const position = ref({ top: 0, left: 0 })
|
||||
|
||||
@@ -203,6 +209,7 @@ const displayModalEdit = (shouldDisplay: boolean) => {
|
||||
const editEnvironment = (environmentIndex: "Global") => {
|
||||
editingEnvironmentIndex.value = environmentIndex
|
||||
action.value = "edit"
|
||||
editingVariableName.value = ""
|
||||
displayModalEdit(true)
|
||||
}
|
||||
|
||||
@@ -232,6 +239,9 @@ const removeSelectedEnvironment = () => {
|
||||
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
editingVariableName.value = ""
|
||||
editingVariableValue.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
defineActionHandler("modals.environment.new", () => {
|
||||
@@ -243,11 +253,19 @@ defineActionHandler("modals.environment.delete-selected", () => {
|
||||
showConfirmRemoveEnvModal.value = true
|
||||
})
|
||||
|
||||
const additionalVars = ref<Environment["variables"]>([])
|
||||
|
||||
const envVars = () => [...globalEnv.value, ...additionalVars.value]
|
||||
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
envName === "Global" && editEnvironment("Global")
|
||||
"modals.global.environment.update",
|
||||
({ variables, isSecret }) => {
|
||||
if (variables) {
|
||||
additionalVars.value = variables
|
||||
}
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
editEnvironment("Global")
|
||||
editingVariableName.value = "Global"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -16,76 +16,103 @@
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<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 class="my-4 flex flex-col border border-divider rounded">
|
||||
<div
|
||||
v-for="({ id, env }, index) in vars"
|
||||
:key="`variable-${id}-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
v-if="evnExpandError"
|
||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||
>
|
||||
<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"
|
||||
: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>
|
||||
{{ t("environment.nested_overflow") }}
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="vars.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||
<template #actions>
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
@@ -112,8 +139,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import { clone } from "lodash-es"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { ComputedRef, computed, ref, watch } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
@@ -136,12 +163,16 @@ import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { environmentsStore } from "~/newstore/environments"
|
||||
import { platform } from "~/platform"
|
||||
import { useService } from "dioc/vue"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
env: {
|
||||
key: string
|
||||
value: string
|
||||
key: string
|
||||
secret: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +186,7 @@ const props = withDefaults(
|
||||
action: "edit" | "new"
|
||||
editingEnvironmentIndex?: number | "Global" | null
|
||||
editingVariableName?: string | null
|
||||
isSecretOptionSelected?: boolean
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
@@ -162,6 +194,7 @@ const props = withDefaults(
|
||||
action: "edit",
|
||||
editingEnvironmentIndex: null,
|
||||
editingVariableName: null,
|
||||
isSecretOptionSelected: false,
|
||||
envVars: () => [],
|
||||
}
|
||||
)
|
||||
@@ -172,11 +205,55 @@ const emit = defineEmits<{
|
||||
|
||||
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 editingID = ref<string>("")
|
||||
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>(
|
||||
IconTrash2,
|
||||
1000
|
||||
@@ -184,14 +261,23 @@ const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||
|
||||
const globalVars = useReadonlyStream(globalEnv$, [])
|
||||
|
||||
type SelectedEnv = "variables" | "secret"
|
||||
|
||||
const selectedEnvOption = ref<SelectedEnv>("variables")
|
||||
|
||||
const workingEnv = computed(() => {
|
||||
if (props.editingEnvironmentIndex === "Global") {
|
||||
const vars =
|
||||
props.editingVariableName === "Global"
|
||||
? props.envVars()
|
||||
: getGlobalVariables()
|
||||
return {
|
||||
name: "Global",
|
||||
variables: getGlobalVariables(),
|
||||
variables: vars,
|
||||
} as Environment
|
||||
} else if (props.action === "new") {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
name: "",
|
||||
variables: props.envVars(),
|
||||
}
|
||||
@@ -214,6 +300,7 @@ const evnExpandError = computed(() => {
|
||||
|
||||
return pipe(
|
||||
variables,
|
||||
A.filter(({ secret }) => !secret),
|
||||
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
||||
)
|
||||
})
|
||||
@@ -239,11 +326,29 @@ watch(
|
||||
(show) => {
|
||||
if (show) {
|
||||
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(
|
||||
workingEnv.value?.variables ?? [],
|
||||
A.map((e) => ({
|
||||
A.mapWithIndex((index, e) => ({
|
||||
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 = () => {
|
||||
vars.value = []
|
||||
vars.value = vars.value.filter((e) =>
|
||||
selectedEnvOption.value === "secret" ? !e.env.secret : e.env.secret
|
||||
)
|
||||
|
||||
clearIcon.value = IconDone
|
||||
toast.success(`${t("state.cleared")}`)
|
||||
}
|
||||
@@ -262,12 +370,16 @@ const addEnvironmentVariable = () => {
|
||||
env: {
|
||||
key: "",
|
||||
value: "",
|
||||
secret: selectedEnvOption.value === "secret",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const removeEnvironmentVariable = (index: number) => {
|
||||
vars.value.splice(index, 1)
|
||||
const removeEnvironmentVariable = (id: number) => {
|
||||
const index = vars.value.findIndex((e) => e.id === id)
|
||||
if (index !== -1) {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const saveEnvironment = () => {
|
||||
@@ -276,7 +388,7 @@ const saveEnvironment = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const filterdVariables = pipe(
|
||||
const filteredVariables = pipe(
|
||||
vars.value,
|
||||
A.filterMap(
|
||||
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 = {
|
||||
v: 1,
|
||||
id: uniqueId(),
|
||||
name: editingName.value,
|
||||
variables: filterdVariables,
|
||||
variables,
|
||||
}
|
||||
|
||||
if (props.action === "new") {
|
||||
// Creating a new environment
|
||||
createEnvironment(editingName.value, environmentUpdated.variables)
|
||||
createEnvironment(
|
||||
editingName.value,
|
||||
environmentUpdated.variables,
|
||||
editingID.value
|
||||
)
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "MY_ENV",
|
||||
index: envList.value.length - 1,
|
||||
@@ -332,6 +473,7 @@ const saveEnvironment = () => {
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
selectedEnvOption.value = "variables"
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -135,6 +135,8 @@ import { useToast } from "@composables/toast"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||
import { useService } from "dioc/vue"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -150,6 +152,8 @@ const emit = defineEmits<{
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||
|
||||
const exportEnvironmentAsJSON = () => {
|
||||
const { environment, environmentIndex } = props
|
||||
exportAsJSON(environment, environmentIndex)
|
||||
@@ -168,6 +172,7 @@ const removeEnvironment = () => {
|
||||
if (props.environmentIndex === null) return
|
||||
if (props.environmentIndex !== "Global") {
|
||||
deleteEnvironment(props.environmentIndex, props.environment.id)
|
||||
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||
}
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
:action="action"
|
||||
:editing-environment-index="editingEnvironmentIndex"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:is-secret-option-selected="secretOptionSelected"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
@@ -99,6 +100,7 @@ const showModalDetails = ref(false)
|
||||
const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironmentIndex = ref<number | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
const secretOptionSelected = ref(false)
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
action.value = "new"
|
||||
@@ -120,18 +122,23 @@ const editEnvironment = (environmentIndex: number) => {
|
||||
}
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
editingVariableName.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
({ envName, variableName, isSecret }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const envIndex: number = environments.value.findIndex(
|
||||
(environment: Environment) => {
|
||||
return environment.name === envName
|
||||
}
|
||||
)
|
||||
if (envName !== "Global") editEnvironment(envIndex)
|
||||
if (envName !== "Global") {
|
||||
editEnvironment(envIndex)
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -16,90 +16,112 @@
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<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 class="my-4 flex flex-col border border-divider rounded">
|
||||
<div
|
||||
v-for="({ id, env }, index) in vars"
|
||||
:key="`variable-${id}-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
v-if="evnExpandError"
|
||||
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||
>
|
||||
<input
|
||||
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>
|
||||
{{ t("environment.nested_overflow") }}
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="vars.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
v-if="isViewer"
|
||||
disabled
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
|
||||
<template #actions>
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/environments"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="!isViewer"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="clearIcon"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="!isViewer"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconPlus"
|
||||
:title="t('add.new')"
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
<template v-if="!isViewer" #footer>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
@@ -119,7 +141,7 @@
|
||||
</template>
|
||||
|
||||
<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 A from "fp-ts/Array"
|
||||
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 IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { platform } from "~/platform"
|
||||
import { useService } from "dioc/vue"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
env: {
|
||||
key: string
|
||||
value: string
|
||||
secret: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +189,7 @@ const props = withDefaults(
|
||||
editingTeamId: string | undefined
|
||||
editingVariableName?: string | null
|
||||
isViewer?: boolean
|
||||
isSecretOptionSelected?: boolean
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
@@ -172,6 +199,7 @@ const props = withDefaults(
|
||||
editingTeamId: "",
|
||||
editingVariableName: null,
|
||||
isViewer: false,
|
||||
isSecretOptionSelected: false,
|
||||
envVars: () => [],
|
||||
}
|
||||
)
|
||||
@@ -182,11 +210,59 @@ const emit = defineEmits<{
|
||||
|
||||
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 editingID = ref<string | null>(null)
|
||||
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>(
|
||||
IconTrash2,
|
||||
1000
|
||||
@@ -215,22 +291,34 @@ watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
editingName.value = props.editingEnvironment?.environment.name ?? null
|
||||
selectedEnvOption.value = props.isSecretOptionSelected
|
||||
? "secret"
|
||||
: "variables"
|
||||
if (props.action === "new") {
|
||||
editingName.value = null
|
||||
vars.value = pipe(
|
||||
props.envVars() ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
A.map((e) => ({
|
||||
id: idTicker.value++,
|
||||
env: clone(e),
|
||||
}))
|
||||
)
|
||||
} else if (props.editingEnvironment !== null) {
|
||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
||||
editingID.value = props.editingEnvironment.id
|
||||
vars.value = pipe(
|
||||
props.editingEnvironment.environment.variables ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
A.mapWithIndex((index, e) => ({
|
||||
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: {
|
||||
key: "",
|
||||
value: "",
|
||||
secret: selectedEnvOption.value === "secret",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const removeEnvironmentVariable = (index: number) => {
|
||||
vars.value.splice(index, 1)
|
||||
const removeEnvironmentVariable = (id: number) => {
|
||||
const index = vars.value.findIndex((e) => e.id === id)
|
||||
if (index !== -1) {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
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") {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_ENVIRONMENT",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
await pipe(
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
props.editingTeamId,
|
||||
editingName.value
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
hideModal()
|
||||
toast.success(`${t("environment.created")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
if (!props.isViewer) {
|
||||
await pipe(
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(environmentUpdated.variables),
|
||||
props.editingTeamId,
|
||||
environmentUpdated.name
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
isLoading.value = false
|
||||
},
|
||||
(res) => {
|
||||
const envID = res.createTeamEnvironment.id
|
||||
if (envID) {
|
||||
secretEnvironmentService.addSecretEnvironment(
|
||||
envID,
|
||||
secretVariables
|
||||
)
|
||||
}
|
||||
hideModal()
|
||||
toast.success(`${t("environment.created")}`)
|
||||
isLoading.value = false
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
} else {
|
||||
if (!props.editingEnvironment) {
|
||||
console.error("No Environment Found")
|
||||
return
|
||||
}
|
||||
|
||||
await pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
props.editingEnvironment.id,
|
||||
editingName.value
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
hideModal()
|
||||
toast.success(`${t("environment.updated")}`)
|
||||
}
|
||||
if (editingID.value) {
|
||||
secretEnvironmentService.addSecretEnvironment(
|
||||
editingID.value,
|
||||
secretVariables
|
||||
)
|
||||
)()
|
||||
|
||||
// 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
|
||||
@@ -331,6 +473,7 @@ const saveEnvironment = async () => {
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
selectedEnvOption.value = "variables"
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
</span>
|
||||
<span>
|
||||
<tippy
|
||||
v-if="!isViewer"
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
@@ -57,6 +56,7 @@
|
||||
/>
|
||||
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="`${t('action.duplicate')}`"
|
||||
@@ -69,6 +69,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="exportAsJsonEl"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('export.as_json')}`"
|
||||
@@ -81,6 +82,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="`${t('action.delete')}`"
|
||||
@@ -124,6 +126,8 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||
import { useService } from "dioc/vue"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -137,6 +141,8 @@ const emit = defineEmits<{
|
||||
(e: "edit-environment"): void
|
||||
}>()
|
||||
|
||||
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const exportEnvironmentAsJSON = () =>
|
||||
@@ -161,6 +167,7 @@ const removeEnvironment = () => {
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("team_environment.deleted")}`)
|
||||
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
:editing-environment="editingEnvironment"
|
||||
:editing-team-id="team?.id"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:is-secret-option-selected="secretOptionSelected"
|
||||
:is-viewer="team?.myRole === 'VIEWER'"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
@@ -148,6 +149,7 @@ const showModalDetails = ref(false)
|
||||
const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
const secretOptionSelected = ref(false)
|
||||
|
||||
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
||||
|
||||
@@ -171,6 +173,8 @@ const editEnvironment = (environment: TeamEnvironment | null) => {
|
||||
}
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironment.value = null
|
||||
editingVariableName.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
@@ -187,12 +191,15 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
|
||||
defineActionHandler(
|
||||
"modals.team.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
({ envName, variableName, isSecret }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const teamEnvToEdit = props.teamEnvironments.find(
|
||||
(environment) => environment.environment.name === envName
|
||||
)
|
||||
if (teamEnvToEdit) editEnvironment(teamEnvToEdit)
|
||||
if (teamEnvToEdit) {
|
||||
editEnvironment(teamEnvToEdit)
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -31,17 +31,6 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
label="None"
|
||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'None'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'none'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isRootCollection"
|
||||
label="Inherit"
|
||||
@@ -54,6 +43,17 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="None"
|
||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'None'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'none'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="Basic Auth"
|
||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||
@@ -284,7 +284,7 @@ const authActive = pluckRef(auth, "authActive")
|
||||
|
||||
const clearContent = () => {
|
||||
auth.value = {
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlHeaders')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -315,6 +315,8 @@ import { commonHeaders } from "~/helpers/headers"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { objRemoveKey } from "~/helpers/functional/object"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { HoppGQLHeader } from "~/helpers/graphql"
|
||||
import { throwError } from "~/helpers/functional/error"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
@@ -338,7 +340,7 @@ const request = useVModel(props, "modelValue", emit)
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
const linewrapEnabled = ref(false)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlHeaders")
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
|
||||
@@ -353,7 +355,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -61,9 +61,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlQuery')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -112,6 +112,8 @@ import {
|
||||
socketDisconnect,
|
||||
subscriptionState,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
// Template refs
|
||||
const queryEditor = ref<any | null>(null)
|
||||
@@ -137,7 +139,7 @@ const prettifyQueryIcon = refAutoReset<
|
||||
typeof IconWand | typeof IconCheck | typeof IconInfo
|
||||
>(IconWand, 1000)
|
||||
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlQuery")
|
||||
|
||||
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
||||
|
||||
@@ -184,7 +186,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "graphql",
|
||||
placeholder: `${t("request.query")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: createGQLQueryLinter(schema),
|
||||
completer: queryCompleter(schema),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-1 flex-col">
|
||||
<div class="h-full">
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky top-0 bg-primary z-10 border-b-0"
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="
|
||||
toggleNestedSetting('WRAP_LINES', 'graphqlResponseBody')
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
@@ -70,7 +72,9 @@
|
||||
</tippy>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
|
||||
<div class="h-full">
|
||||
<div ref="schemaEditor"></div>
|
||||
</div>
|
||||
</div>
|
||||
<component
|
||||
:is="response[0].error.component"
|
||||
@@ -99,6 +103,8 @@ import { useI18n } from "@composables/i18n"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||
import {
|
||||
useCopyInterface,
|
||||
@@ -133,8 +139,8 @@ const responseString = computed(() => {
|
||||
})
|
||||
|
||||
const schemaEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlResponseBody")
|
||||
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
schemaEditor,
|
||||
@@ -143,7 +149,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -127,9 +127,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlSchema')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -145,11 +145,9 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="schemaString"
|
||||
ref="schemaEditor"
|
||||
class="flex flex-1 flex-col"
|
||||
></div>
|
||||
<div v-if="schemaString" class="h-full relative w-full">
|
||||
<div ref="schemaEditor" class="absolute inset-0"></div>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-else
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
@@ -202,6 +200,8 @@ import {
|
||||
subscriptionFields,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { platform } from "~/platform"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
||||
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
||||
@@ -349,7 +349,7 @@ const handleJumpToType = async (type: GraphQLType) => {
|
||||
}
|
||||
|
||||
const schemaEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlSchema")
|
||||
|
||||
useCodemirror(
|
||||
schemaEditor,
|
||||
@@ -358,7 +358,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "graphql",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlVariables')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -67,7 +67,9 @@
|
||||
/>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -93,6 +95,8 @@ import {
|
||||
socketDisconnect,
|
||||
subscriptionState,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -114,7 +118,7 @@ const variableString = useVModel(props, "modelValue", emit)
|
||||
|
||||
const variableEditor = ref<any | null>(null)
|
||||
|
||||
const linewrapEnabled = ref(false)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlVariables")
|
||||
|
||||
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
@@ -131,7 +135,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
placeholder: `${t("request.variables")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: computed(() =>
|
||||
variableString.value.length > 0 ? jsonLinter : null
|
||||
|
||||
@@ -331,7 +331,8 @@ const deleteHistory = (entry: HistoryEntry) => {
|
||||
const addToCollection = (entry: HistoryEntry) => {
|
||||
if (props.page === "rest") {
|
||||
invokeAction("request.save-as", {
|
||||
request: entry.request,
|
||||
requestType: "rest",
|
||||
request: entry.request as HoppRESTRequest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,17 +31,6 @@
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
label="None"
|
||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'None'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'none'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isRootCollection"
|
||||
label="Inherit"
|
||||
@@ -54,6 +43,17 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="None"
|
||||
:icon="authName === 'None' ? IconCircleDot : IconCircle"
|
||||
:active="authName === 'None'"
|
||||
@click="
|
||||
() => {
|
||||
auth.authType = 'none'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="Basic Auth"
|
||||
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
|
||||
@@ -265,7 +265,7 @@ const authActive = pluckRef(auth, "authActive")
|
||||
|
||||
const clearContent = () => {
|
||||
auth.value = {
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +86,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'codeGen')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
@@ -161,6 +161,8 @@ import cloneDeep from "lodash-es/cloneDeep"
|
||||
import { platform } from "~/platform"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -187,6 +189,8 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
const requestCode = computed(() => {
|
||||
const aggregateEnvs = getAggregateEnvs()
|
||||
const env: Environment = {
|
||||
v: 1,
|
||||
id: "env",
|
||||
name: "Env",
|
||||
variables: aggregateEnvs,
|
||||
}
|
||||
@@ -222,7 +226,7 @@ const requestCode = computed(() => {
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const generatedCode = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "codeGen")
|
||||
|
||||
useCodemirror(
|
||||
generatedCode,
|
||||
@@ -231,7 +235,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
v-if="bulkMode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpHeaders')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -49,7 +49,9 @@
|
||||
/>
|
||||
</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>
|
||||
<draggable
|
||||
v-model="workingHeaders"
|
||||
@@ -332,6 +334,8 @@ import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
|
||||
const t = useI18n()
|
||||
@@ -346,7 +350,7 @@ const idTicker = ref(0)
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
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)
|
||||
|
||||
@@ -371,7 +375,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter,
|
||||
completer: null,
|
||||
@@ -553,7 +557,7 @@ const clearContent = () => {
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
||||
|
||||
const computedHeaders = computed(() =>
|
||||
getComputedHeaders(request.value, aggregateEnvs.value).map(
|
||||
getComputedHeaders(request.value, aggregateEnvs.value, false).map(
|
||||
(header, index) => ({
|
||||
id: `header-${index}`,
|
||||
...header,
|
||||
@@ -606,7 +610,8 @@ const inheritedProperties = computed(() => {
|
||||
const computedAuthHeader = getComputedAuthHeaders(
|
||||
aggregateEnvs.value,
|
||||
request.value,
|
||||
props.inheritedProperties.auth.inheritedAuth
|
||||
props.inheritedProperties.auth.inheritedAuth,
|
||||
false
|
||||
)[0]
|
||||
|
||||
if (
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'importCurl')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
@@ -96,6 +96,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { platform } from "~/platform"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -106,7 +108,7 @@ const tabs = useService(RESTTabService)
|
||||
const curl = ref("")
|
||||
|
||||
const curlEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "importCurl")
|
||||
|
||||
const props = defineProps<{ show: boolean; text: string }>()
|
||||
|
||||
@@ -117,7 +119,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/x-sh",
|
||||
placeholder: `${t("request.enter_curl")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -3,14 +3,25 @@
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="oidcDiscoveryURL"
|
||||
:styles="
|
||||
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
|
||||
"
|
||||
placeholder="OpenID Connect Discovery URL"
|
||||
/>
|
||||
</div>
|
||||
<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 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 class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
||||
@@ -44,6 +55,7 @@ import { useToast } from "@composables/toast"
|
||||
import { tokenRequest } from "~/helpers/oauth"
|
||||
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { computed } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -66,10 +78,16 @@ watch(
|
||||
)
|
||||
|
||||
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
|
||||
const hasOIDCURL = computed(() => {
|
||||
return oidcDiscoveryURL.value
|
||||
})
|
||||
|
||||
const authURL = pluckRef(auth, "authURL")
|
||||
|
||||
const accessTokenURL = pluckRef(auth, "accessTokenURL")
|
||||
const hasAccessTokenOrAuthURL = computed(() => {
|
||||
return accessTokenURL.value || authURL.value
|
||||
})
|
||||
|
||||
const clientID = pluckRef(auth, "clientID")
|
||||
|
||||
@@ -88,13 +106,11 @@ function translateTokenRequestError(error: string) {
|
||||
}
|
||||
|
||||
const handleAccessTokenRequest = async () => {
|
||||
if (
|
||||
oidcDiscoveryURL.value === "" &&
|
||||
(authURL.value === "" || accessTokenURL.value === "")
|
||||
) {
|
||||
if (!oidcDiscoveryURL.value && !(authURL.value || accessTokenURL.value)) {
|
||||
toast.error(`${t("error.incomplete_config_urls")}`)
|
||||
return
|
||||
}
|
||||
|
||||
const envs = getCombinedEnvVariables()
|
||||
const envVars = [...envs.selected, ...envs.global]
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
v-if="bulkMode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpParams')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -44,7 +44,9 @@
|
||||
/>
|
||||
</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>
|
||||
<draggable
|
||||
v-model="workingParams"
|
||||
@@ -205,6 +207,8 @@ import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
@@ -217,7 +221,7 @@ const idTicker = ref(0)
|
||||
const bulkMode = ref(false)
|
||||
const bulkParams = ref("")
|
||||
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)
|
||||
|
||||
@@ -228,7 +232,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter,
|
||||
completer: null,
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpPreRequest')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div ref="preRequestEditor" class="h-full"></div>
|
||||
<div class="w-2/3 border-r border-dividerLight h-full relative">
|
||||
<div ref="preRequestEditor" class="h-full absolute inset-0"></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"
|
||||
@@ -72,6 +72,8 @@ import linter from "~/helpers/editor/linting/preRequest"
|
||||
import completer from "~/helpers/editor/completion/preRequest"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -85,7 +87,7 @@ const emit = defineEmits<{
|
||||
const preRequestScript = useVModel(props, "modelValue", emit)
|
||||
|
||||
const preRequestEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpPreRequest")
|
||||
|
||||
useCodemirror(
|
||||
preRequestEditor,
|
||||
@@ -93,7 +95,7 @@ useCodemirror(
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
placeholder: `${t("preRequest.javascript_code")}`,
|
||||
},
|
||||
linter,
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpRequestBody')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="
|
||||
@@ -59,7 +59,9 @@
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -85,6 +87,8 @@ import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
import jsonLinter from "~/helpers/editor/linting/json"
|
||||
import { readFileAsText } from "~/helpers/functional/files"
|
||||
import xmlFormat from "xml-formatter"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
type PossibleContentTypes = Exclude<
|
||||
ValidContentTypes,
|
||||
@@ -122,7 +126,7 @@ const langLinter = computed(() =>
|
||||
isJSONContentType(body.value.contentType) ? jsonLinter : null
|
||||
)
|
||||
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpRequestBody")
|
||||
const rawBodyParameters = ref<any | null>(null)
|
||||
|
||||
const codemirrorValue: Ref<string | undefined> =
|
||||
@@ -148,7 +152,7 @@ useCodemirror(
|
||||
codemirrorValue,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
mode: rawInputEditorLang,
|
||||
placeholder: t("request.raw_body").toString(),
|
||||
},
|
||||
|
||||
@@ -255,7 +255,7 @@ import IconShare2 from "~icons/lucide/share-2"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
||||
import { platform } from "~/platform"
|
||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
@@ -263,6 +263,7 @@ import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
const t = useI18n()
|
||||
const interceptorService = useService(InterceptorService)
|
||||
@@ -326,6 +327,8 @@ const inspectionService = useService(InspectionService)
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
|
||||
const newSendRequest = async () => {
|
||||
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||
toast.error(`${t("empty.endpoint")}`)
|
||||
@@ -341,6 +344,7 @@ const newSendRequest = async () => {
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "rest",
|
||||
strategy: interceptorService.currentInterceptorID.value!,
|
||||
workspaceType: workspaceService.currentWorkspace.value.type,
|
||||
})
|
||||
|
||||
const [cancel, streamPromise] = runRESTRequest$(tab)
|
||||
@@ -395,17 +399,14 @@ const newSendRequest = async () => {
|
||||
}
|
||||
|
||||
const ensureMethodInEndpoint = () => {
|
||||
if (
|
||||
!/^http[s]?:\/\//.test(newEndpoint.value) &&
|
||||
!newEndpoint.value.startsWith("<<")
|
||||
) {
|
||||
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
|
||||
const endpoint = newEndpoint.value.trim()
|
||||
tab.value.document.request.endpoint = endpoint
|
||||
if (!/^http[s]?:\/\//.test(endpoint) && !endpoint.startsWith("<<")) {
|
||||
const domain = endpoint.split(/[/:#?]+/)[0]
|
||||
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
|
||||
tab.value.document.request.endpoint =
|
||||
"http://" + tab.value.document.request.endpoint
|
||||
tab.value.document.request.endpoint = "http://" + endpoint
|
||||
} else {
|
||||
tab.value.document.request.endpoint =
|
||||
"https://" + tab.value.document.request.endpoint
|
||||
tab.value.document.request.endpoint = "https://" + endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,25 +578,12 @@ defineActionHandler("request.share-request", shareRequest)
|
||||
defineActionHandler("request.method.next", cycleDownMethod)
|
||||
defineActionHandler("request.method.prev", cycleUpMethod)
|
||||
defineActionHandler("request.save", saveRequest)
|
||||
defineActionHandler(
|
||||
"request.save-as",
|
||||
(
|
||||
req:
|
||||
| {
|
||||
requestType: "rest"
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
| {
|
||||
requestType: "gql"
|
||||
request: HoppGQLRequest
|
||||
}
|
||||
) => {
|
||||
showSaveRequestModal.value = true
|
||||
if (req && req.requestType === "rest") {
|
||||
request.value = req.request
|
||||
}
|
||||
defineActionHandler("request.save-as", (req) => {
|
||||
showSaveRequestModal.value = true
|
||||
if (req?.requestType === "rest") {
|
||||
request.value = req.request
|
||||
}
|
||||
)
|
||||
})
|
||||
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
||||
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
||||
defineActionHandler("request.method.put", () => updateMethod("PUT"))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<HttpResponse v-model:document="tab.document" />
|
||||
<HttpResponse v-model:document="tab.document" :is-embed="false" />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.r="renameAction?.$el.click()"
|
||||
@keyup.s="shareRequestAction?.$el.click()"
|
||||
@keyup.d="duplicateAction?.$el.click()"
|
||||
@keyup.w="closeAction?.$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
|
||||
v-if="isRemovable"
|
||||
ref="closeAction"
|
||||
@@ -99,6 +112,7 @@ import IconXCircle from "~icons/lucide/x-circle"
|
||||
import IconXSquare from "~icons/lucide/x-square"
|
||||
import IconFileEdit from "~icons/lucide/file-edit"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
|
||||
@@ -114,6 +128,7 @@ const emit = defineEmits<{
|
||||
(event: "close-tab"): void
|
||||
(event: "close-other-tabs"): void
|
||||
(event: "duplicate-tab"): void
|
||||
(event: "share-tab-request"): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
@@ -123,4 +138,5 @@ const renameAction = ref<HTMLButtonElement | null>(null)
|
||||
const closeAction = ref<HTMLButtonElement | null>(null)
|
||||
const closeOthersAction = ref<HTMLButtonElement | null>(null)
|
||||
const duplicateAction = ref<HTMLButtonElement | null>(null)
|
||||
const shareRequestAction = ref<HTMLButtonElement | null>(null)
|
||||
</script>
|
||||
|
||||
@@ -211,7 +211,6 @@ import { useI18n } from "@composables/i18n"
|
||||
import {
|
||||
globalEnv$,
|
||||
selectedEnvironmentIndex$,
|
||||
setGlobalEnvVariables,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
@@ -225,6 +224,7 @@ import { useColorMode } from "~/composables/theming"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppTestResult | null | undefined
|
||||
@@ -304,9 +304,10 @@ const globalHasAdditions = computed(() => {
|
||||
|
||||
const addEnvToGlobal = () => {
|
||||
if (!testResults.value?.envDiff.selected.additions) return
|
||||
setGlobalEnvVariables([
|
||||
...globalEnvVars.value,
|
||||
...testResults.value.envDiff.selected.additions,
|
||||
])
|
||||
|
||||
invokeAction("modals.global.environment.update", {
|
||||
variables: testResults.value.envDiff.selected.additions,
|
||||
isSecret: false,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpTest')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div ref="testScriptEditor" class="h-full"></div>
|
||||
<div class="w-2/3 border-r border-dividerLight h-full relative">
|
||||
<div ref="testScriptEditor" class="h-full absolute inset-0"></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"
|
||||
@@ -72,6 +72,8 @@ import linter from "~/helpers/editor/linting/testScript"
|
||||
import completer from "~/helpers/editor/completion/testScript"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -81,7 +83,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const testScript = useVModel(props, "modelValue", emit)
|
||||
const testScriptEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpTest")
|
||||
|
||||
useCodemirror(
|
||||
testScriptEditor,
|
||||
@@ -89,7 +91,7 @@ useCodemirror(
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
placeholder: `${t("test.javascript_code")}`,
|
||||
},
|
||||
linter,
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
v-if="bulkMode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpUrlEncoded')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -44,7 +44,9 @@
|
||||
/>
|
||||
</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>
|
||||
<draggable
|
||||
v-model="workingUrlEncodedParams"
|
||||
@@ -196,6 +198,8 @@ import { useColorMode } from "@composables/theming"
|
||||
import { objRemoveKey } from "~/helpers/functional/object"
|
||||
import { throwError } from "~/helpers/functional/error"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
type Body = HoppRESTReqBody & {
|
||||
contentType: "application/x-www-form-urlencoded"
|
||||
@@ -220,7 +224,7 @@ const idTicker = ref(0)
|
||||
const bulkMode = ref(false)
|
||||
const bulkUrlEncodedParams = ref("")
|
||||
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)
|
||||
|
||||
@@ -231,7 +235,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter,
|
||||
completer: null,
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
@@ -44,11 +44,9 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="!previewEnabled"
|
||||
ref="htmlResponse"
|
||||
class="flex flex-1 flex-col"
|
||||
></div>
|
||||
<div v-show="!previewEnabled" class="h-full">
|
||||
<div ref="htmlResponse" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
<iframe
|
||||
v-show="previewEnabled"
|
||||
ref="previewFrame"
|
||||
@@ -76,6 +74,8 @@ import { useI18n } from "@composables/i18n"
|
||||
import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -84,7 +84,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const htmlResponse = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||
|
||||
const { responseBodyText } = useResponseBody(props.response)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
@@ -104,7 +104,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "htmlmixed",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
@@ -119,11 +119,12 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="jsonResponse"
|
||||
class="flex h-auto h-full flex-1 flex-col"
|
||||
:class="toggleFilter ? 'responseToggleOn' : 'responseToggleOff'"
|
||||
></div>
|
||||
<div class="h-full">
|
||||
<div
|
||||
ref="jsonResponse"
|
||||
:class="toggleFilter ? 'responseToggleOn' : 'responseToggleOff'"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
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"
|
||||
@@ -260,6 +261,8 @@ import {
|
||||
} from "@composables/lens-actions"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||
|
||||
const t = useI18n()
|
||||
@@ -371,8 +374,8 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const jsonResponse = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const { cursor } = useCodemirror(
|
||||
jsonResponse,
|
||||
@@ -381,7 +384,7 @@ const { cursor } = useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
@@ -35,7 +35,9 @@
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -58,6 +60,8 @@ import {
|
||||
import { objFieldMatches } from "~/helpers/functional/object"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -97,7 +101,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
|
||||
|
||||
const rawResponse = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||
|
||||
useCodemirror(
|
||||
rawResponse,
|
||||
@@ -106,7 +110,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
@@ -35,7 +35,9 @@
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -58,6 +60,8 @@ import {
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { objFieldMatches } from "~/helpers/functional/object"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -91,7 +95,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
|
||||
|
||||
const xmlResponse = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||
|
||||
useCodemirror(
|
||||
xmlResponse,
|
||||
@@ -100,7 +104,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/xml",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -130,7 +130,9 @@
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
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"
|
||||
:title="`${timeStamp}`"
|
||||
@click="openInNewTab"
|
||||
@click="customizeSharedRequest()"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
|
||||
@@ -62,7 +62,7 @@
|
||||
:shortcut="['T']"
|
||||
@click="
|
||||
() => {
|
||||
openInNewTab()
|
||||
emit('open-shared-request', parseRequest)
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -128,7 +128,7 @@ const emit = defineEmits<{
|
||||
embedProperties?: string | null
|
||||
): 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)
|
||||
@@ -145,10 +145,6 @@ const requestLabelColor = computed(() =>
|
||||
getMethodLabelColorClassOf(parseRequest.value)
|
||||
)
|
||||
|
||||
const openInNewTab = () => {
|
||||
emit("open-new-tab", parseRequest.value)
|
||||
}
|
||||
|
||||
const customizeSharedRequest = () => {
|
||||
const embedProperties = props.request.properties
|
||||
emit(
|
||||
|
||||
@@ -9,8 +9,14 @@
|
||||
/>
|
||||
</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
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/widgets"
|
||||
@@ -47,7 +53,7 @@
|
||||
:request="request"
|
||||
@customize-shared-request="customizeSharedRequest"
|
||||
@delete-shared-request="deleteSharedRequest"
|
||||
@open-new-tab="openInNewTab"
|
||||
@open-shared-request="openRequestInNewTab"
|
||||
/>
|
||||
<HoppSmartIntersection
|
||||
v-if="hasMoreSharedRequests"
|
||||
@@ -70,7 +76,15 @@
|
||||
:alt="`${t('empty.shared_requests')}`"
|
||||
:text="t('empty.shared_requests')"
|
||||
@drop.stop
|
||||
/>
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonPrimary
|
||||
:label="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
@click="shareRequest()"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartConfirmModal
|
||||
@@ -95,6 +109,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import ShortcodeListAdapter from "~/helpers/shortcode/ShortcodeListAdapter"
|
||||
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) => {
|
||||
if (currentUser.value) {
|
||||
sharedRequestID.value = codeID
|
||||
@@ -434,13 +460,6 @@ const copySharedRequest = (payload: {
|
||||
}
|
||||
}
|
||||
|
||||
const openInNewTab = (request: HoppRESTRequest) => {
|
||||
restTab.createNewTab({
|
||||
isDirty: false,
|
||||
request,
|
||||
})
|
||||
}
|
||||
|
||||
const resolveConfirmModal = (title: string | null) => {
|
||||
if (title === `${t("confirm.remove_shared_request")}`) onDeleteSharedRequest()
|
||||
else {
|
||||
@@ -465,6 +484,13 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
}
|
||||
}
|
||||
|
||||
const openRequestInNewTab = (request: HoppRESTRequest) => {
|
||||
restTab.createNewTab({
|
||||
isDirty: false,
|
||||
request,
|
||||
})
|
||||
}
|
||||
|
||||
defineActionHandler("share.request", ({ request }) => {
|
||||
requestToShare.value = request
|
||||
displayShareRequestModal(true)
|
||||
|
||||
@@ -3,7 +3,18 @@
|
||||
<div
|
||||
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
|
||||
v-else
|
||||
ref="editor"
|
||||
:placeholder="placeholder"
|
||||
class="flex flex-1"
|
||||
@@ -11,7 +22,14 @@
|
||||
@click="emit('click', $event)"
|
||||
@keydown="handleKeystroke"
|
||||
@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
|
||||
:inspection-results="inspectionResults"
|
||||
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 { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||
import {
|
||||
AggregateEnvironment,
|
||||
aggregateEnvsWithSecrets$,
|
||||
} from "~/newstore/environments"
|
||||
import { platform } from "~/platform"
|
||||
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||
import { InspectorResult } from "~/services/inspection"
|
||||
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(
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
styles?: string
|
||||
envs?: { key: string; value: string; source: string }[] | null
|
||||
envs?: Env[] | null
|
||||
focus?: boolean
|
||||
selectTextOnMount?: boolean
|
||||
environmentHighlights?: boolean
|
||||
@@ -80,6 +109,7 @@ const props = withDefaults(
|
||||
autoCompleteSource?: string[]
|
||||
inspectionResults?: InspectorResult[] | undefined
|
||||
contextMenuEnabled?: boolean
|
||||
secret?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
@@ -93,6 +123,7 @@ const props = withDefaults(
|
||||
inspectionResult: undefined,
|
||||
inspectionResults: undefined,
|
||||
contextMenuEnabled: true,
|
||||
secret: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -118,10 +149,27 @@ const showSuggestionPopover = ref(false)
|
||||
const suggestionsMenu = 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, () => {
|
||||
showSuggestionPopover.value = false
|
||||
})
|
||||
|
||||
const toggleSecret = () => {
|
||||
isSecret.value = !isSecret.value
|
||||
}
|
||||
|
||||
//filter autocompleteSource with unique values
|
||||
const uniqueAutoCompleteSource = computed(() => {
|
||||
if (props.autoCompleteSource) {
|
||||
@@ -169,8 +217,6 @@ watch(
|
||||
)
|
||||
|
||||
const handleKeystroke = (ev: KeyboardEvent) => {
|
||||
if (!props.autoCompleteSource) return
|
||||
|
||||
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(ev.key)) {
|
||||
ev.preventDefault()
|
||||
}
|
||||
@@ -307,19 +353,28 @@ watch(
|
||||
let clipboardEv: ClipboardEvent | null = null
|
||||
let pastedValue: string | null = null
|
||||
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
|
||||
AggregateEnvironment[]
|
||||
>
|
||||
|
||||
const envVars = computed(() =>
|
||||
props.envs
|
||||
? props.envs.map((x) => ({
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: x.source,
|
||||
}))
|
||||
const envVars = computed(() => {
|
||||
return props.envs
|
||||
? props.envs.map((x) => {
|
||||
if (x.secret) {
|
||||
return {
|
||||
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
|
||||
)
|
||||
})
|
||||
|
||||
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
||||
|
||||
@@ -363,17 +418,28 @@ const initView = (el: any) => {
|
||||
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 = [
|
||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (props.readonly) {
|
||||
if (readonly) {
|
||||
update.view.contentDOM.inputMode = "none"
|
||||
}
|
||||
}),
|
||||
EditorState.changeFilter.of(() => !props.readonly),
|
||||
EditorState.changeFilter.of(() => !readonly),
|
||||
inputTheme,
|
||||
props.readonly
|
||||
readonly
|
||||
? EditorView.theme({
|
||||
".cm-content": {
|
||||
caretColor: "var(--secondary-dark-color)",
|
||||
@@ -384,6 +450,7 @@ const initView = (el: any) => {
|
||||
})
|
||||
: EditorView.theme({}),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
position: "absolute",
|
||||
}),
|
||||
props.environmentHighlights ? envTooltipPlugin : [],
|
||||
@@ -405,7 +472,8 @@ const initView = (el: any) => {
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
if (props.readonly) return
|
||||
if (readonly) return
|
||||
|
||||
if (update.docChanged) {
|
||||
const prevValue = clone(cachedValue.value)
|
||||
|
||||
@@ -454,14 +522,7 @@ const initView = (el: any) => {
|
||||
history(),
|
||||
keymap.of([...historyKeymap]),
|
||||
]
|
||||
|
||||
view.value = new EditorView({
|
||||
parent: el,
|
||||
state: EditorState.create({
|
||||
doc: props.modelValue,
|
||||
extensions,
|
||||
}),
|
||||
})
|
||||
return extensions
|
||||
}
|
||||
|
||||
const triggerTextSelection = () => {
|
||||
@@ -474,11 +535,11 @@ const triggerTextSelection = () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (editor.value) {
|
||||
if (!view.value) initView(editor.value)
|
||||
if (props.selectTextOnMount) triggerTextSelection()
|
||||
if (props.focus) view.value?.focus()
|
||||
platform.ui?.onCodemirrorInstanceMount?.(editor.value)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
placeholder,
|
||||
tooltips,
|
||||
} from "@codemirror/view"
|
||||
import {
|
||||
Extension,
|
||||
@@ -269,6 +270,7 @@ export function useCodemirror(
|
||||
basicSetup,
|
||||
baseTheme,
|
||||
syntaxHighlighting(baseHighlightStyle, { fallback: true }),
|
||||
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
@@ -318,6 +320,7 @@ export function useCodemirror(
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
EditorView.domEventHandlers({
|
||||
scroll(event) {
|
||||
if (event.target && options.contextMenuEnabled) {
|
||||
@@ -359,6 +362,10 @@ export function useCodemirror(
|
||||
run: indentLess,
|
||||
},
|
||||
]),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
position: "absolute",
|
||||
}),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
additionalExts.of(options.additionalExts ?? []),
|
||||
]
|
||||
|
||||
@@ -13,7 +13,36 @@ export function useSetting<K extends keyof SettingsDef>(
|
||||
settingsStore.dispatch({
|
||||
dispatcher: "applySetting",
|
||||
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
|
||||
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,
|
||||
},
|
||||
})
|
||||
@@ -35,7 +64,9 @@ export function useSettingStatic<K extends keyof SettingsDef>(
|
||||
settingsStore.dispatch({
|
||||
dispatcher: "applySetting",
|
||||
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
|
||||
value,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -30,6 +30,13 @@ import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import {
|
||||
SecretEnvironmentService,
|
||||
SecretVariable,
|
||||
} from "~/services/secret-environment.service"
|
||||
import { getService } from "~/modules/dioc"
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
const getTestableBody = (
|
||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||
@@ -58,15 +65,63 @@ const getTestableBody = (
|
||||
return x
|
||||
}
|
||||
|
||||
const combineEnvVariables = (env: {
|
||||
const combineEnvVariables = (envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}) => [...env.selected, ...env.global]
|
||||
}) => [...envs.selected, ...envs.global]
|
||||
|
||||
export const executedResponses$ = new Subject<
|
||||
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$(
|
||||
tab: Ref<HoppTab<HoppRESTDocument>>
|
||||
): [
|
||||
@@ -154,15 +209,36 @@ export function runRESTRequest$(
|
||||
)
|
||||
|
||||
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
|
||||
tab.value.document.response = res
|
||||
|
||||
tab.value.document.testResults = translateToSandboxTestResults(
|
||||
runResult.right
|
||||
const updatedRunResult = {
|
||||
...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 (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
|
||||
) {
|
||||
@@ -173,8 +249,10 @@ export function runRESTRequest$(
|
||||
updateEnvironment(
|
||||
environmentsStore.value.selectedEnvironmentIndex.index,
|
||||
{
|
||||
...env,
|
||||
variables: runResult.right.envs.selected,
|
||||
name: env.name,
|
||||
v: 1,
|
||||
id: env.id ?? "",
|
||||
variables: updatedRunResult.envs.selected,
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
@@ -186,7 +264,7 @@ export function runRESTRequest$(
|
||||
})
|
||||
pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(runResult.right.envs.selected),
|
||||
JSON.stringify(updatedRunResult.envs.selected),
|
||||
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
|
||||
env.name
|
||||
)
|
||||
@@ -275,7 +353,6 @@ function translateToSandboxTestResults(
|
||||
|
||||
const globals = cloneDeep(getGlobalVariables())
|
||||
const env = getCurrentEnvironment()
|
||||
|
||||
return {
|
||||
description: "",
|
||||
expectResults: testDesc.tests.expectResults,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { HoppRESTDocument } from "./rest/document"
|
||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { Environment, HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||
import { HoppGQLSaveContext } from "./graphql/document"
|
||||
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
||||
@@ -43,6 +43,7 @@ export type HoppAction =
|
||||
| "modals.environment.new" // Add new environment
|
||||
| "modals.environment.delete-selected" // Delete Selected 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.new" // Add new team
|
||||
| "modals.team.edit" // Edit selected team
|
||||
@@ -66,6 +67,13 @@ export type HoppAction =
|
||||
| "user.login" // Login to Hoppscotch
|
||||
| "user.logout" // Log out of Hoppscotch
|
||||
| "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
|
||||
@@ -86,13 +94,19 @@ type HoppActionArgsMap = {
|
||||
}
|
||||
text: string | null
|
||||
}
|
||||
"modals.global.environment.update": {
|
||||
variables?: Environment["variables"]
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.my.environment.edit": {
|
||||
envName: string
|
||||
variableName?: string
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.team.environment.edit": {
|
||||
envName: string
|
||||
variableName?: string
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.team.delete": {
|
||||
teamId: string
|
||||
@@ -112,6 +126,7 @@ type HoppActionArgsMap = {
|
||||
requestType: "gql"
|
||||
request: HoppGQLRequest
|
||||
}
|
||||
| undefined
|
||||
"request.open-tab": {
|
||||
tab: RESTOptionTabs | GQLOptionTabs
|
||||
}
|
||||
@@ -121,7 +136,6 @@ type HoppActionArgsMap = {
|
||||
"tab.duplicate-tab": {
|
||||
tabID?: string
|
||||
}
|
||||
|
||||
"gql.request.open": {
|
||||
request: HoppGQLRequest
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -145,27 +171,26 @@ export type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
|
||||
/**
|
||||
* Resolves the argument type for a given HoppAction
|
||||
*/
|
||||
type ArgOfHoppAction<A extends HoppAction | HoppActionWithArgs> =
|
||||
A extends HoppActionWithArgs ? HoppActionArgsMap[A] : undefined
|
||||
type ArgOfHoppAction<A extends HoppAction> = A extends HoppActionWithArgs
|
||||
? HoppActionArgsMap[A]
|
||||
: undefined
|
||||
|
||||
/**
|
||||
* Resolves the action function for a given HoppAction, used by action handler function defs
|
||||
*/
|
||||
type ActionFunc<A extends HoppAction | HoppActionWithArgs> =
|
||||
A extends HoppActionWithArgs ? (arg: ArgOfHoppAction<A>) => void : () => void
|
||||
type ActionFunc<A extends HoppAction> = A extends HoppActionWithArgs
|
||||
? (arg: ArgOfHoppAction<A>, trigger?: InvocationTriggers) => void
|
||||
: (_?: undefined, trigger?: InvocationTriggers) => void
|
||||
|
||||
type BoundActionList = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[A in HoppAction | HoppActionWithArgs]?: Array<ActionFunc<A>>
|
||||
[A in HoppAction]?: Array<ActionFunc<A>>
|
||||
}
|
||||
|
||||
const boundActions: BoundActionList = reactive({})
|
||||
|
||||
export const activeActions$ = new BehaviorSubject<
|
||||
(HoppAction | HoppActionWithArgs)[]
|
||||
>([])
|
||||
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
|
||||
|
||||
export function bindAction<A extends HoppAction | HoppActionWithArgs>(
|
||||
export function bindAction<A extends HoppAction>(
|
||||
action: A,
|
||||
handler: ActionFunc<A>
|
||||
) {
|
||||
@@ -179,27 +204,33 @@ export function bindAction<A extends HoppAction | HoppActionWithArgs>(
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
export type InvocationTriggers = "keypress" | "mouseclick"
|
||||
|
||||
type InvokeActionFunc = {
|
||||
(action: HoppActionWithNoArgs, args?: undefined): void
|
||||
(
|
||||
action: HoppActionWithOptionalArgs,
|
||||
args?: undefined,
|
||||
trigger?: InvocationTriggers
|
||||
): void
|
||||
<A extends HoppActionWithArgs>(action: A, args: HoppActionArgsMap[A]): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a action, triggering action handlers if any registered.
|
||||
* The second argument parameter is optional if your action has no args required
|
||||
* Invokes an action, triggering action handlers if any registered.
|
||||
* The second and third arguments are optional
|
||||
* @param action The action to fire
|
||||
* @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 = <
|
||||
A extends HoppAction | HoppActionWithArgs,
|
||||
>(
|
||||
export const invokeAction: InvokeActionFunc = <A extends HoppAction>(
|
||||
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,
|
||||
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 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,
|
||||
handler: ActionFunc<A>,
|
||||
isActive: Ref<boolean> | undefined = undefined
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
mutation CreateTeamEnvironment($variables: String!,$teamID: ID!,$name: String!){
|
||||
createTeamEnvironment( variables: $variables ,teamID: $teamID ,name: $name){
|
||||
mutation CreateTeamEnvironment(
|
||||
$variables: String!
|
||||
$teamID: ID!
|
||||
$name: String!
|
||||
) {
|
||||
createTeamEnvironment(variables: $variables, teamID: $teamID, name: $name) {
|
||||
variables
|
||||
name
|
||||
teamID
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const samples = [
|
||||
method: "GET",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
body: {
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: rawKeyValueEntriesToString([
|
||||
@@ -149,7 +149,7 @@ const samples = [
|
||||
method: "GET",
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
@@ -166,7 +166,7 @@ const samples = [
|
||||
method: "POST",
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost:1111/hello/world/?buzz",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
body: {
|
||||
contentType: "application/json",
|
||||
body: `{\n "foo": "bar"\n}`,
|
||||
@@ -189,7 +189,7 @@ const samples = [
|
||||
method: "GET",
|
||||
name: "Untitled",
|
||||
endpoint: "https://example.com/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
@@ -217,7 +217,7 @@ const samples = [
|
||||
method: "POST",
|
||||
name: "Untitled",
|
||||
endpoint: "https://bing.com/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
body: {
|
||||
contentType: "multipart/form-data",
|
||||
body: [
|
||||
@@ -301,7 +301,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost:9900/",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
@@ -345,7 +345,7 @@ const samples = [
|
||||
endpoint: "https://hoppscotch.io/?io",
|
||||
auth: {
|
||||
authActive: true,
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
},
|
||||
body: {
|
||||
contentType: null,
|
||||
@@ -380,7 +380,7 @@ const samples = [
|
||||
endpoint: "https://someshadywebsite.com/questionable/path/?so",
|
||||
auth: {
|
||||
authActive: true,
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
},
|
||||
body: {
|
||||
contentType: "multipart/form-data",
|
||||
@@ -441,7 +441,7 @@ const samples = [
|
||||
endpoint: "http://localhost/",
|
||||
auth: {
|
||||
authActive: true,
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
},
|
||||
body: {
|
||||
contentType: "multipart/form-data",
|
||||
@@ -473,7 +473,7 @@ const samples = [
|
||||
method: "GET",
|
||||
name: "Untitled",
|
||||
endpoint: "https://hoppscotch.io/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
@@ -528,7 +528,7 @@ const samples = [
|
||||
method: "GET",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
body: {
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: rawKeyValueEntriesToString([
|
||||
@@ -573,7 +573,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "POST",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
headers: [
|
||||
{
|
||||
active: true,
|
||||
@@ -615,7 +615,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://muxueqz.top/skybook.html",
|
||||
method: "GET",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
headers: [],
|
||||
body: { contentType: null, body: null },
|
||||
params: [],
|
||||
@@ -629,7 +629,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "POST",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
headers: [],
|
||||
body: {
|
||||
contentType: "multipart/form-data",
|
||||
@@ -653,7 +653,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "http://127.0.0.1/",
|
||||
method: "CUSTOMMETHOD",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
headers: [],
|
||||
body: {
|
||||
contentType: null,
|
||||
@@ -670,7 +670,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "GET",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
headers: [
|
||||
{
|
||||
active: true,
|
||||
@@ -693,7 +693,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "GET",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
headers: [],
|
||||
body: {
|
||||
contentType: null,
|
||||
@@ -710,7 +710,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://example.org/",
|
||||
method: "HEAD",
|
||||
auth: { authType: "none", authActive: true },
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
headers: [],
|
||||
body: {
|
||||
contentType: null,
|
||||
@@ -756,7 +756,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
@@ -777,7 +777,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
@@ -797,7 +797,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "http://192.168.0.24:8080/ping",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
@@ -817,7 +817,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://example.com/",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
|
||||
@@ -12,14 +12,17 @@ import { parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { StreamSubscriberFunc } from "@composables/stream"
|
||||
import {
|
||||
AggregateEnvironment,
|
||||
aggregateEnvs$,
|
||||
getAggregateEnvs,
|
||||
aggregateEnvsWithSecrets$,
|
||||
getAggregateEnvsWithSecrets,
|
||||
getCurrentEnvironment,
|
||||
getSelectedEnvironmentType,
|
||||
} from "~/newstore/environments"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import IconUser from "~icons/lucide/user?raw"
|
||||
import IconUsers from "~icons/lucide/users?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
|
||||
|
||||
@@ -28,6 +31,8 @@ const HOPP_ENV_HIGHLIGHT =
|
||||
const HOPP_ENV_HIGHLIGHT_FOUND = "env-found"
|
||||
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "env-not-found"
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
hoverTooltip(
|
||||
(view, pos, side) => {
|
||||
@@ -66,7 +71,27 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
|
||||
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)
|
||||
|
||||
@@ -83,12 +108,25 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
editIcon.className =
|
||||
"ml-2 cursor-pointer text-accent hover:text-accentDark"
|
||||
editIcon.addEventListener("click", () => {
|
||||
const isPersonalEnv =
|
||||
envName === "Global" || selectedEnvType !== "TEAM_ENV"
|
||||
const action = isPersonalEnv ? "my" : "team"
|
||||
invokeAction(`modals.${action}.environment.edit`, {
|
||||
envName,
|
||||
let invokeActionType:
|
||||
| "modals.my.environment.edit"
|
||||
| "modals.team.environment.edit"
|
||||
| "modals.global.environment.update" = "modals.my.environment.edit"
|
||||
|
||||
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,
|
||||
isSecret: tooltipEnv?.secret,
|
||||
})
|
||||
})
|
||||
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
||||
@@ -171,11 +209,10 @@ export class HoppEnvironmentPlugin {
|
||||
subscribeToStream: StreamSubscriberFunc,
|
||||
private editorView: Ref<EditorView | undefined>
|
||||
) {
|
||||
this.envs = getAggregateEnvs()
|
||||
this.envs = getAggregateEnvsWithSecrets()
|
||||
|
||||
subscribeToStream(aggregateEnvs$, (envs) => {
|
||||
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
|
||||
this.envs = envs
|
||||
|
||||
this.editorView.value?.dispatch({
|
||||
effects: this.compartment.reconfigure([
|
||||
cursorTooltipField(this.envs),
|
||||
|
||||
@@ -171,9 +171,6 @@ export const baseTheme = EditorView.theme({
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
".cm-foldPlaceholder": {
|
||||
backgroundColor: "var(--divider-light-color)",
|
||||
color: "var(--secondary-dark-color)",
|
||||
@@ -320,9 +317,6 @@ export const inputTheme = EditorView.theme({
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
".cm-foldPlaceholder": {
|
||||
backgroundColor: "var(--divider-light-color)",
|
||||
color: "var(--secondary-dark-color)",
|
||||
|
||||
@@ -27,7 +27,7 @@ export const getDefaultGQLRequest = (): HoppGQLRequest => ({
|
||||
}`,
|
||||
query: DEFAULT_QUERY,
|
||||
auth: {
|
||||
authType: "none",
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -12,8 +12,6 @@ const getEnvironmentJson = (
|
||||
? cloneDeep(environmentObj.environment)
|
||||
: cloneDeep(environmentObj)
|
||||
|
||||
delete newEnvironment.id
|
||||
|
||||
const environmentId =
|
||||
environmentIndex || environmentIndex === 0
|
||||
? environmentIndex
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
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 { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
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) => {
|
||||
const parsedContent = safeParseJSON(content, true)
|
||||
|
||||
@@ -25,7 +16,9 @@ export const hoppEnvImporter = (content: string) => {
|
||||
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) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
|
||||
@@ -4,8 +4,9 @@ import * as O from "fp-ts/Option"
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
import { z } from "zod"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { NonSecretEnvironment } from "@hoppscotch/data"
|
||||
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
const insomniaResourcesSchema = z.object({
|
||||
resources: z.array(
|
||||
@@ -56,16 +57,18 @@ export const insomniaEnvImporter = (content: string) => {
|
||||
return { ...envResource, data: stringifiedData }
|
||||
})
|
||||
|
||||
const environments: Environment[] = []
|
||||
const environments: NonSecretEnvironment[] = []
|
||||
|
||||
insomniaEnvs.forEach((insomniaEnv) => {
|
||||
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
|
||||
|
||||
if (parsedInsomniaEnv.success) {
|
||||
const environment: Environment = {
|
||||
const environment: NonSecretEnvironment = {
|
||||
id: uniqueId(),
|
||||
v: 1,
|
||||
name: parsedInsomniaEnv.data.name,
|
||||
variables: Object.entries(parsedInsomniaEnv.data.data).map(
|
||||
([key, value]) => ({ key, value })
|
||||
([key, value]) => ({ key, value, secret: false })
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
import { z } from "zod"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
const postmanEnvSchema = z.object({
|
||||
name: z.string(),
|
||||
@@ -34,12 +35,14 @@ export const postmanEnvImporter = (content: string) => {
|
||||
const postmanEnv = validationResult.data
|
||||
|
||||
const environment: Environment = {
|
||||
id: uniqueId(),
|
||||
v: 1,
|
||||
name: postmanEnv.name,
|
||||
variables: [],
|
||||
}
|
||||
|
||||
postmanEnv.values.forEach(({ key, value }) =>
|
||||
environment.variables.push({ key, value })
|
||||
environment.variables.push({ key, value, secret: false })
|
||||
)
|
||||
|
||||
return TE.right(environment)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user