Compare commits
23 Commits
pr/JoelJac
...
feat/admin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70665dae03 | ||
|
|
efc98588d9 | ||
|
|
619bdf85f3 | ||
|
|
2509545dea | ||
|
|
df2d5995fd | ||
|
|
3c2b48a635 | ||
|
|
a0d40c8776 | ||
|
|
3c7a2401ae | ||
|
|
9543369ff3 | ||
|
|
fd5abd59fb | ||
|
|
8f6ca169ce | ||
|
|
2eab86476e | ||
|
|
b53cbb093c | ||
|
|
2bde3f8b02 | ||
|
|
da606f5a96 | ||
|
|
2a667a74f0 | ||
|
|
a4c889e38d | ||
|
|
9ceef43c74 | ||
|
|
abaddd94a5 | ||
|
|
88bca2057a | ||
|
|
3ff6cc53bb | ||
|
|
1df2520bf0 | ||
|
|
5368c52aab |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.12.6",
|
||||
"version": "2023.12.3",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -34,14 +34,12 @@
|
||||
"@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",
|
||||
@@ -59,7 +57,6 @@
|
||||
"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,7 +33,6 @@ 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)
|
||||
@@ -311,25 +310,6 @@ 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,8 +24,6 @@ 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: [
|
||||
@@ -98,8 +96,6 @@ import { ScheduleModule } from '@nestjs/schedule';
|
||||
UserCollectionModule,
|
||||
ShortcodeModule,
|
||||
InfraConfigModule,
|
||||
PosthogModule,
|
||||
ScheduleModule.forRoot(),
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -711,9 +711,3 @@ 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,7 +3,6 @@ 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',
|
||||
@@ -105,12 +104,3 @@ 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,12 +19,7 @@ import {
|
||||
} from 'src/errors';
|
||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
ServiceStatus,
|
||||
generateAnalyticsUserId,
|
||||
getConfiguredSSOProviders,
|
||||
stopApp,
|
||||
} from './helper';
|
||||
import { ServiceStatus, getConfiguredSSOProviders, stopApp } from './helper';
|
||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||
import { AuthProvider } from 'src/auth/helper';
|
||||
|
||||
@@ -80,14 +75,6 @@ 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',
|
||||
@@ -244,22 +231,6 @@ 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 `A user has invited you to join a team workspace in Hoppscotch`;
|
||||
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
|
||||
|
||||
case 'user-invitation':
|
||||
return 'Sign in to Hoppscotch';
|
||||
|
||||
@@ -27,12 +27,6 @@
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a.nohighlight {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
@@ -464,7 +458,7 @@
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<h1>Hi there,</h1>
|
||||
<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>
|
||||
<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>
|
||||
<!-- Action -->
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
@@ -490,7 +484,7 @@
|
||||
Welcome aboard, <br />
|
||||
Your friends at Hoppscotch
|
||||
</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>
|
||||
<p><strong>P.S.</strong> If you don't associate with {{invitee}} or {{invite_team_name}}, 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,25 +22,19 @@
|
||||
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;
|
||||
@@ -53,13 +47,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -67,7 +61,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -75,7 +69,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -83,12 +77,12 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
@@ -97,25 +91,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;
|
||||
@@ -130,7 +124,7 @@
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
@@ -138,7 +132,7 @@
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
@@ -146,7 +140,7 @@
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
@@ -154,21 +148,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;
|
||||
@@ -177,31 +171,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;
|
||||
@@ -212,33 +206,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;
|
||||
@@ -247,7 +241,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -256,50 +250,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;
|
||||
@@ -309,7 +303,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -319,16 +313,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;
|
||||
@@ -337,7 +331,7 @@
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -346,7 +340,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -356,7 +350,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -366,11 +360,11 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
@@ -380,25 +374,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,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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 {}
|
||||
@@ -1,58 +0,0 @@
|
||||
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,8 +13,6 @@ 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',
|
||||
}
|
||||
|
||||
@@ -31,6 +29,5 @@ 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,13 +58,9 @@
|
||||
"@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",
|
||||
"verzod": "^0.2.2",
|
||||
"zod": "^3.22.4"
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,247 +3,138 @@ import { ExecException } from "child_process";
|
||||
import { HoppErrorCode } from "../../types/errors";
|
||||
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
|
||||
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);
|
||||
describe("Test 'hopp test <file>' command:", () => {
|
||||
test("No collection file path provided.", 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);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
test("Collection file is invalid JSON.", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"malformed-collection.json"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const args = `test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
)}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
test("Invalid arguement.", async () => {
|
||||
const args = "invalid-arg";
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Collection file not JSON type.", async () => {
|
||||
const args = `test ${getTestJsonFilePath("notjson.txt")}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
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);
|
||||
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toMatchObject(<ExecException>{
|
||||
code: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
|
||||
const args = "invalid-arg";
|
||||
const { stderr } = await runCLI(args);
|
||||
test("No errors occured (exit code 0).", async () => {
|
||||
const args = `test ${getTestJsonFilePath("passes.json")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Supports inheriting headers and authorization set at the root collection", async () => {
|
||||
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`;
|
||||
const { error } = await runCLI(args);
|
||||
|
||||
expect(error).toBeNull();
|
||||
})
|
||||
|
||||
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>("FILE_NOT_FOUND");
|
||||
});
|
||||
|
||||
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>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
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>("MALFORMED_COLLECTION");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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("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:", () => {
|
||||
describe("Supplied environment export file validations", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
describe("Test 'hopp test <file> --env <file>' command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
|
||||
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env`;
|
||||
const { stderr } = await runCLI(args);
|
||||
test("No env file path provided.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --env`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
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");
|
||||
});
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
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");
|
||||
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");
|
||||
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
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");
|
||||
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");
|
||||
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-coll.json", "collection")}`;
|
||||
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
|
||||
"passes.json"
|
||||
)}`;
|
||||
|
||||
test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
|
||||
test("No value passed to delay flag.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
@@ -251,7 +142,7 @@ describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => {
|
||||
test("Invalid value passed to delay flag.", async () => {
|
||||
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
@@ -259,17 +150,10 @@ describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Successfully performs delayed request execution for a valid delay value", async () => {
|
||||
test("Valid value passed to delay flag.", 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"id": 123,
|
||||
"v": "1",
|
||||
"name": "secret-envs",
|
||||
"values": [
|
||||
{
|
||||
"key": "secretVar",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "regularVar",
|
||||
"secret": false,
|
||||
"value": "regular-variable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"v": 0,
|
||||
"name": "Response body sample",
|
||||
"variables": [
|
||||
{
|
||||
@@ -35,4 +34,4 @@
|
||||
"value": "<<salutation>> <<fullName>>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import { resolve } from "path";
|
||||
|
||||
import { ExecResponse } from "./types";
|
||||
|
||||
export const runCLI = (args: string, options = {}): Promise<ExecResponse> =>
|
||||
export const runCLI = (args: string): Promise<ExecResponse> =>
|
||||
{
|
||||
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
|
||||
const command = `node ${CLI_PATH} ${args}`
|
||||
|
||||
return new Promise((resolve) =>
|
||||
exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,12 +25,7 @@ export const getErrorCode = (out: string) => {
|
||||
return ansiTrimmedStr.split(" ")[0];
|
||||
};
|
||||
|
||||
export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => {
|
||||
const kindDir = {
|
||||
collection: "collections",
|
||||
environment: "environments",
|
||||
}[kind];
|
||||
|
||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`);
|
||||
export const getTestJsonFilePath = (file: string) => {
|
||||
const filePath = resolve(__dirname, `../../src/__tests__/samples/${file}`);
|
||||
return filePath;
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface RequestStack {
|
||||
*/
|
||||
export interface RequestConfig extends AxiosRequestConfig {
|
||||
supported: boolean;
|
||||
displayUrl?: string
|
||||
}
|
||||
|
||||
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
@@ -31,7 +30,6 @@ 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,42 +1,34 @@
|
||||
import { Environment } from "@hoppscotch/data";
|
||||
import { entityReference } from "verzod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { error } from "../../types/errors";
|
||||
import {
|
||||
HoppEnvKeyPairObject,
|
||||
HoppEnvs,
|
||||
HoppEnvPair,
|
||||
HoppEnvs
|
||||
HoppEnvKeyPairObject,
|
||||
HoppEnvExportObject,
|
||||
HoppBulkEnvExportObject,
|
||||
} 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<Environment["variables"][number] | HoppEnvPair> = [];
|
||||
|
||||
// The legacy key-value pair format that is still supported
|
||||
const envPairs: Array<HoppEnvPair> = [];
|
||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
||||
const HoppBulkEnvExportObjectResult =
|
||||
HoppBulkEnvExportObject.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
|
||||
// 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.type === "err") {
|
||||
// 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)) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
@@ -44,8 +36,8 @@ export async function parseEnvsData(path: string) {
|
||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
} else if (HoppEnvExportObjectResult.type === "ok") {
|
||||
envPairs.push(...HoppEnvExportObjectResult.value.variables);
|
||||
} else if (HoppEnvExportObjectResult.success) {
|
||||
envPairs.push(...HoppEnvExportObjectResult.data.variables);
|
||||
}
|
||||
|
||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { z } from "zod";
|
||||
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { TestReport } from "../interfaces/response";
|
||||
import { HoppCLIError } from "./errors";
|
||||
import { z } from "zod";
|
||||
|
||||
export type FormDataEntry = {
|
||||
key: string;
|
||||
value: string | Blob;
|
||||
};
|
||||
|
||||
export type HoppEnvPair = Environment["variables"][number];
|
||||
export type HoppEnvPair = { key: string; value: string };
|
||||
|
||||
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
|
||||
|
||||
// Shape of the single environment export object that is exported from the app.
|
||||
export const HoppEnvExportObject = z.object({
|
||||
name: z.string(),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
// Shape of the bulk environment export object that is exported from the app.
|
||||
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
|
||||
|
||||
export type HoppEnvs = {
|
||||
global: HoppEnvPair[];
|
||||
selected: HoppEnvPair[];
|
||||
|
||||
@@ -176,7 +176,7 @@ export const printRequestRunner = {
|
||||
*/
|
||||
start: (requestConfig: RequestConfig) => {
|
||||
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
||||
const ENDPOINT = requestConfig.displayUrl || requestConfig.url;
|
||||
const ENDPOINT = requestConfig.url;
|
||||
|
||||
process.stdout.write(`${METHOD} ${ENDPOINT}`);
|
||||
},
|
||||
|
||||
@@ -36,10 +36,7 @@ import { toFormData } from "./mutators";
|
||||
export const preRequestScriptRunner = (
|
||||
request: HoppRESTRequest,
|
||||
envs: HoppEnvs
|
||||
): TE.TaskEither<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> =>
|
||||
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
|
||||
pipe(
|
||||
TE.of(request),
|
||||
TE.chain(({ preRequestScript }) =>
|
||||
@@ -71,10 +68,7 @@ export const preRequestScriptRunner = (
|
||||
export function getEffectiveRESTRequest(
|
||||
request: HoppRESTRequest,
|
||||
environment: Environment
|
||||
): E.Either<
|
||||
HoppCLIError,
|
||||
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
|
||||
> {
|
||||
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
|
||||
const envVariables = environment.variables;
|
||||
|
||||
// Parsing final headers with applied ENVs.
|
||||
@@ -168,30 +162,12 @@ 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({
|
||||
effectiveRequest: {
|
||||
...request,
|
||||
effectiveFinalURL,
|
||||
effectiveFinalDisplayURL,
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
},
|
||||
updatedEnvs: { global: [], selected: envVariables },
|
||||
...request,
|
||||
effectiveFinalURL,
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { 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,38 +29,6 @@ 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.
|
||||
@@ -70,7 +38,6 @@ const processEnvs = (envs: HoppEnvs) => {
|
||||
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
|
||||
const config: RequestConfig = {
|
||||
supported: true,
|
||||
displayUrl: req.effectiveFinalDisplayURL
|
||||
};
|
||||
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
|
||||
const reqParams = finalParams(req);
|
||||
@@ -254,13 +221,9 @@ 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, processedEnvs)();
|
||||
const preRequestRes = await preRequestScriptRunner(request, envs)();
|
||||
if (E.isLeft(preRequestRes)) {
|
||||
printPreRequestRunner.fail();
|
||||
|
||||
@@ -268,8 +231,8 @@ export const processRequest =
|
||||
report.errors.push(preRequestRes.left);
|
||||
report.result = report.result && false;
|
||||
} else {
|
||||
// Updating effective-request and consuming updated envs after pre-request script execution
|
||||
({ effectiveRequest, updatedEnvs } = preRequestRes.right);
|
||||
// Updating effective-request
|
||||
effectiveRequest = preRequestRes.right;
|
||||
}
|
||||
|
||||
// Creating request-config for request-runner.
|
||||
@@ -307,7 +270,7 @@ export const processRequest =
|
||||
const testScriptParams = getTestScriptParams(
|
||||
_requestRunnerRes,
|
||||
request,
|
||||
updatedEnvs
|
||||
envs
|
||||
);
|
||||
|
||||
// Executing test-runner.
|
||||
|
||||
@@ -429,11 +429,6 @@ pre.ace_editor {
|
||||
}
|
||||
}
|
||||
|
||||
.splitpanes__pane {
|
||||
@apply will-change-auto;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply before:absolute;
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"go_back": "Go back",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"hide_secret": "Hide secret",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
"less": "Less",
|
||||
@@ -44,7 +43,6 @@
|
||||
"search": "Search",
|
||||
"send": "Send",
|
||||
"share": "Share",
|
||||
"show_secret": "Show secret",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
@@ -240,7 +238,6 @@
|
||||
"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",
|
||||
@@ -272,8 +269,6 @@
|
||||
"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",
|
||||
@@ -282,7 +277,6 @@
|
||||
"updated": "Environment updated",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"variables":"Variables",
|
||||
"variable_list": "Variable List"
|
||||
},
|
||||
"error": {
|
||||
@@ -419,8 +413,6 @@
|
||||
"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": {
|
||||
@@ -897,7 +889,6 @@
|
||||
"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.6",
|
||||
"version": "2023.12.3",
|
||||
"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', undefined, 'mouseclick')"
|
||||
@click="invokeAction('modals.search.toggle')"
|
||||
>
|
||||
<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="closeSpotlightModal"
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col border-b border-divider transition">
|
||||
@@ -86,36 +86,35 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { ref, computed, watch } from "vue"
|
||||
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 { useI18n } from "@composables/i18n"
|
||||
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()
|
||||
|
||||
@@ -291,17 +290,4 @@ 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: "none",
|
||||
authActive: true,
|
||||
authType: "inherit",
|
||||
authActive: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'cookie')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
@@ -102,8 +102,6 @@ import {
|
||||
useCopyResponse,
|
||||
useDownloadResponse,
|
||||
} from "~/composables/lens-actions"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
// TODO: Build Managed Mode!
|
||||
|
||||
@@ -124,7 +122,7 @@ const toast = useToast()
|
||||
|
||||
const cookieEditor = ref<HTMLElement>()
|
||||
const rawCookieString = ref("")
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "cookie")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
cookieEditor,
|
||||
@@ -133,7 +131,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
||||
t("environment.value")
|
||||
}}</label>
|
||||
<SmartEnvInput
|
||||
<input
|
||||
v-model="editingValue"
|
||||
type="text"
|
||||
class="input"
|
||||
@@ -154,14 +154,12 @@ 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, NonSecretEnvironment } from "@hoppscotch/data"
|
||||
import { Environment } 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?: NonSecretEnvironment
|
||||
globalEnv?: Environment
|
||||
) => {
|
||||
// if there's a global env, add them to the store
|
||||
if (globalEnv) {
|
||||
globalEnv.variables.forEach(({ key, value, secret }) =>
|
||||
addGlobalEnvVariable({ key, value, secret })
|
||||
)
|
||||
globalEnv.variables.forEach(({ key, value }) => {
|
||||
addGlobalEnvVariable({ key, value })
|
||||
})
|
||||
}
|
||||
|
||||
if (props.environmentType === "MY_ENV") {
|
||||
|
||||
@@ -210,10 +210,7 @@
|
||||
{{ variable.key }}
|
||||
</span>
|
||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||
<template v-if="variable.secret"> ******** </template>
|
||||
<template v-else>
|
||||
{{ variable.value }}
|
||||
</template>
|
||||
{{ variable.value }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
|
||||
@@ -268,10 +265,7 @@
|
||||
{{ variable.key }}
|
||||
</span>
|
||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||
<template v-if="variable.secret"> ******** </template>
|
||||
<template v-else>
|
||||
{{ variable.value }}
|
||||
</template>
|
||||
{{ variable.value }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -485,20 +479,15 @@ 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 =
|
||||
@@ -593,7 +582,9 @@ const environmentVariables = computed(() => {
|
||||
})
|
||||
|
||||
const editGlobalEnv = () => {
|
||||
invokeAction("modals.global.environment.update", {})
|
||||
invokeAction("modals.my.environment.edit", {
|
||||
envName: "Global",
|
||||
})
|
||||
}
|
||||
|
||||
const editEnv = () => {
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
:action="action"
|
||||
:editing-environment-index="editingEnvironmentIndex"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:env-vars="envVars"
|
||||
:is-secret-option-selected="secretOptionSelected"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsAdd
|
||||
@@ -39,7 +37,7 @@
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="showConfirmRemoveEnvModal"
|
||||
:title="`${t('confirm.remove_environment')}`"
|
||||
:title="t('confirm.remove_team')"
|
||||
@hide-modal="showConfirmRemoveEnvModal = false"
|
||||
@resolve="removeSelectedEnvironment()"
|
||||
/>
|
||||
@@ -69,7 +67,6 @@ 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()
|
||||
@@ -91,8 +88,6 @@ const environmentType = ref<EnvironmentsChooseType>({
|
||||
const globalEnv = useReadonlyStream(globalEnv$, [])
|
||||
|
||||
const globalEnvironment = computed(() => ({
|
||||
v: 1 as const,
|
||||
id: "Global",
|
||||
name: "Global",
|
||||
variables: globalEnv.value,
|
||||
}))
|
||||
@@ -191,7 +186,6 @@ 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 })
|
||||
|
||||
@@ -209,7 +203,6 @@ const displayModalEdit = (shouldDisplay: boolean) => {
|
||||
const editEnvironment = (environmentIndex: "Global") => {
|
||||
editingEnvironmentIndex.value = environmentIndex
|
||||
action.value = "edit"
|
||||
editingVariableName.value = ""
|
||||
displayModalEdit(true)
|
||||
}
|
||||
|
||||
@@ -239,9 +232,6 @@ const removeSelectedEnvironment = () => {
|
||||
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
editingVariableName.value = ""
|
||||
editingVariableValue.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
defineActionHandler("modals.environment.new", () => {
|
||||
@@ -253,19 +243,11 @@ defineActionHandler("modals.environment.delete-selected", () => {
|
||||
showConfirmRemoveEnvModal.value = true
|
||||
})
|
||||
|
||||
const additionalVars = ref<Environment["variables"]>([])
|
||||
|
||||
const envVars = () => [...globalEnv.value, ...additionalVars.value]
|
||||
|
||||
defineActionHandler(
|
||||
"modals.global.environment.update",
|
||||
({ variables, isSecret }) => {
|
||||
if (variables) {
|
||||
additionalVars.value = variables
|
||||
}
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
editEnvironment("Global")
|
||||
editingVariableName.value = "Global"
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
envName === "Global" && editEnvironment("Global")
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -16,103 +16,76 @@
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="my-4 flex flex-col border border-divider rounded">
|
||||
<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 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>
|
||||
<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>
|
||||
</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
|
||||
v-for="({ id, env }, index) in vars"
|
||||
: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"
|
||||
:select-text-on-mount="env.key === editingVariableName"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:envs="liveEnvs"
|
||||
:name="'value' + index"
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
id="variable"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@click="removeEnvironmentVariable(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -139,8 +112,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 IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { ComputedRef, computed, ref, watch } from "vue"
|
||||
import { clone } from "lodash-es"
|
||||
import { 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"
|
||||
@@ -163,16 +136,12 @@ 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: {
|
||||
value: string
|
||||
key: string
|
||||
secret: boolean
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +155,6 @@ const props = withDefaults(
|
||||
action: "edit" | "new"
|
||||
editingEnvironmentIndex?: number | "Global" | null
|
||||
editingVariableName?: string | null
|
||||
isSecretOptionSelected?: boolean
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
@@ -194,7 +162,6 @@ const props = withDefaults(
|
||||
action: "edit",
|
||||
editingEnvironmentIndex: null,
|
||||
editingVariableName: null,
|
||||
isSecretOptionSelected: false,
|
||||
envVars: () => [],
|
||||
}
|
||||
)
|
||||
@@ -205,55 +172,11 @@ 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: "", secret: false } },
|
||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
||||
])
|
||||
|
||||
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
|
||||
@@ -261,23 +184,14 @@ 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: vars,
|
||||
variables: getGlobalVariables(),
|
||||
} as Environment
|
||||
} else if (props.action === "new") {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
name: "",
|
||||
variables: props.envVars(),
|
||||
}
|
||||
@@ -300,7 +214,6 @@ const evnExpandError = computed(() => {
|
||||
|
||||
return pipe(
|
||||
variables,
|
||||
A.filter(({ secret }) => !secret),
|
||||
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
|
||||
)
|
||||
})
|
||||
@@ -326,29 +239,11 @@ 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.mapWithIndex((index, e) => ({
|
||||
A.map((e) => ({
|
||||
id: idTicker.value++,
|
||||
env: {
|
||||
key: e.key,
|
||||
value: e.secret
|
||||
? secretEnvironmentService.getSecretEnvironmentVariable(
|
||||
props.editingEnvironmentIndex === "Global"
|
||||
? "Global"
|
||||
: workingEnv.value?.id,
|
||||
index
|
||||
)?.value ?? ""
|
||||
: e.value,
|
||||
secret: e.secret,
|
||||
},
|
||||
env: clone(e),
|
||||
}))
|
||||
)
|
||||
}
|
||||
@@ -356,10 +251,7 @@ watch(
|
||||
)
|
||||
|
||||
const clearContent = () => {
|
||||
vars.value = vars.value.filter((e) =>
|
||||
selectedEnvOption.value === "secret" ? !e.env.secret : e.env.secret
|
||||
)
|
||||
|
||||
vars.value = []
|
||||
clearIcon.value = IconDone
|
||||
toast.success(`${t("state.cleared")}`)
|
||||
}
|
||||
@@ -370,16 +262,12 @@ const addEnvironmentVariable = () => {
|
||||
env: {
|
||||
key: "",
|
||||
value: "",
|
||||
secret: selectedEnvOption.value === "secret",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const removeEnvironmentVariable = (id: number) => {
|
||||
const index = vars.value.findIndex((e) => e.id === id)
|
||||
if (index !== -1) {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
const removeEnvironmentVariable = (index: number) => {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const saveEnvironment = () => {
|
||||
@@ -388,7 +276,7 @@ const saveEnvironment = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const filteredVariables = pipe(
|
||||
const filterdVariables = pipe(
|
||||
vars.value,
|
||||
A.filterMap(
|
||||
flow(
|
||||
@@ -398,43 +286,14 @@ 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,
|
||||
variables: filterdVariables,
|
||||
}
|
||||
|
||||
if (props.action === "new") {
|
||||
// Creating a new environment
|
||||
createEnvironment(
|
||||
editingName.value,
|
||||
environmentUpdated.variables,
|
||||
editingID.value
|
||||
)
|
||||
createEnvironment(editingName.value, environmentUpdated.variables)
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "MY_ENV",
|
||||
index: envList.value.length - 1,
|
||||
@@ -473,7 +332,6 @@ const saveEnvironment = () => {
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
selectedEnvOption.value = "variables"
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -135,8 +135,6 @@ 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()
|
||||
@@ -152,8 +150,6 @@ const emit = defineEmits<{
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||
|
||||
const exportEnvironmentAsJSON = () => {
|
||||
const { environment, environmentIndex } = props
|
||||
exportAsJSON(environment, environmentIndex)
|
||||
@@ -172,7 +168,6 @@ 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,7 +67,6 @@
|
||||
:action="action"
|
||||
:editing-environment-index="editingEnvironmentIndex"
|
||||
:editing-variable-name="editingVariableName"
|
||||
:is-secret-option-selected="secretOptionSelected"
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
@@ -100,7 +99,6 @@ 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"
|
||||
@@ -122,23 +120,18 @@ const editEnvironment = (environmentIndex: number) => {
|
||||
}
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironmentIndex.value = null
|
||||
editingVariableName.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
"modals.my.environment.edit",
|
||||
({ envName, variableName, isSecret }) => {
|
||||
({ envName, variableName }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const envIndex: number = environments.value.findIndex(
|
||||
(environment: Environment) => {
|
||||
return environment.name === envName
|
||||
}
|
||||
)
|
||||
if (envName !== "Global") {
|
||||
editEnvironment(envIndex)
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
}
|
||||
if (envName !== "Global") editEnvironment(envIndex)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -16,112 +16,90 @@
|
||||
@submit="saveEnvironment"
|
||||
/>
|
||||
|
||||
<div class="my-4 flex flex-col border border-divider rounded">
|
||||
<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 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>
|
||||
<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>
|
||||
</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
|
||||
v-for="({ id, env }, index) in vars"
|
||||
: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"
|
||||
:class="isViewer && 'opacity-25'"
|
||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
||||
:name="'param' + index"
|
||||
:disabled="isViewer"
|
||||
/>
|
||||
<SmartEnvInput
|
||||
v-model="env.value"
|
||||
:select-text-on-mount="env.key === editingVariableName"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:envs="liveEnvs"
|
||||
:name="'value' + index"
|
||||
:readonly="isViewer"
|
||||
/>
|
||||
<div v-if="!isViewer" class="flex">
|
||||
<HoppButtonSecondary
|
||||
id="variable"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@click="removeEnvironmentVariable(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<template v-if="!isViewer" #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="`${t('action.save')}`"
|
||||
@@ -141,7 +119,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComputedRef, computed, ref, watch } from "vue"
|
||||
import { 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"
|
||||
@@ -163,17 +141,13 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +163,6 @@ const props = withDefaults(
|
||||
editingTeamId: string | undefined
|
||||
editingVariableName?: string | null
|
||||
isViewer?: boolean
|
||||
isSecretOptionSelected?: boolean
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
@@ -199,7 +172,6 @@ const props = withDefaults(
|
||||
editingTeamId: "",
|
||||
editingVariableName: null,
|
||||
isViewer: false,
|
||||
isSecretOptionSelected: false,
|
||||
envVars: () => [],
|
||||
}
|
||||
)
|
||||
@@ -210,59 +182,11 @@ 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: "", secret: false } },
|
||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
||||
])
|
||||
|
||||
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
|
||||
@@ -291,34 +215,22 @@ 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) => ({
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
id: idTicker.value++,
|
||||
env: clone(e),
|
||||
}))
|
||||
)
|
||||
} else if (props.editingEnvironment !== null) {
|
||||
editingID.value = props.editingEnvironment.id
|
||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
||||
vars.value = pipe(
|
||||
props.editingEnvironment.environment.variables ?? [],
|
||||
A.mapWithIndex((index, e) => ({
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
id: idTicker.value++,
|
||||
env: {
|
||||
key: e.key,
|
||||
value: e.secret
|
||||
? secretEnvironmentService.getSecretEnvironmentVariable(
|
||||
editingID.value ?? "",
|
||||
index
|
||||
)?.value ?? ""
|
||||
: e.value,
|
||||
secret: e.secret,
|
||||
},
|
||||
env: clone(e),
|
||||
}))
|
||||
)
|
||||
}
|
||||
@@ -338,16 +250,12 @@ const addEnvironmentVariable = () => {
|
||||
env: {
|
||||
key: "",
|
||||
value: "",
|
||||
secret: selectedEnvOption.value === "secret",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const removeEnvironmentVariable = (id: number) => {
|
||||
const index = vars.value.findIndex((e) => e.id === id)
|
||||
if (index !== -1) {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
const removeEnvironmentVariable = (index: number) => {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const isLoading = ref(false)
|
||||
@@ -370,102 +278,52 @@ 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",
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
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")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
} else {
|
||||
if (!props.editingEnvironment) {
|
||||
console.error("No Environment Found")
|
||||
return
|
||||
}
|
||||
|
||||
if (editingID.value) {
|
||||
secretEnvironmentService.addSecretEnvironment(
|
||||
editingID.value,
|
||||
secretVariables
|
||||
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 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
|
||||
@@ -473,7 +331,6 @@ const saveEnvironment = async () => {
|
||||
|
||||
const hideModal = () => {
|
||||
editingName.value = null
|
||||
selectedEnvOption.value = "variables"
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</span>
|
||||
<span>
|
||||
<tippy
|
||||
v-if="!isViewer"
|
||||
ref="options"
|
||||
interactive
|
||||
trigger="click"
|
||||
@@ -56,7 +57,6 @@
|
||||
/>
|
||||
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
:label="`${t('action.duplicate')}`"
|
||||
@@ -69,7 +69,6 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="exportAsJsonEl"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('export.as_json')}`"
|
||||
@@ -82,7 +81,6 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isViewer"
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="`${t('action.delete')}`"
|
||||
@@ -126,8 +124,6 @@ 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()
|
||||
@@ -141,8 +137,6 @@ const emit = defineEmits<{
|
||||
(e: "edit-environment"): void
|
||||
}>()
|
||||
|
||||
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const exportEnvironmentAsJSON = () =>
|
||||
@@ -167,7 +161,6 @@ const removeEnvironment = () => {
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("team_environment.deleted")}`)
|
||||
secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
@@ -105,7 +105,6 @@
|
||||
: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)"
|
||||
/>
|
||||
@@ -149,7 +148,6 @@ 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")
|
||||
|
||||
@@ -173,8 +171,6 @@ const editEnvironment = (environment: TeamEnvironment | null) => {
|
||||
}
|
||||
const resetSelectedData = () => {
|
||||
editingEnvironment.value = null
|
||||
editingVariableName.value = ""
|
||||
secretOptionSelected.value = false
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
@@ -191,15 +187,12 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
|
||||
defineActionHandler(
|
||||
"modals.team.environment.edit",
|
||||
({ envName, variableName, isSecret }) => {
|
||||
({ envName, variableName }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const teamEnvToEdit = props.teamEnvironments.find(
|
||||
(environment) => environment.environment.name === envName
|
||||
)
|
||||
if (teamEnvToEdit) {
|
||||
editEnvironment(teamEnvToEdit)
|
||||
secretOptionSelected.value = isSecret ?? false
|
||||
}
|
||||
if (teamEnvToEdit) editEnvironment(teamEnvToEdit)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -31,6 +31,17 @@
|
||||
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"
|
||||
@@ -43,17 +54,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<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: "inherit",
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlHeaders')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -315,8 +315,6 @@ 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"
|
||||
@@ -340,7 +338,7 @@ const request = useVModel(props, "modelValue", emit)
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlHeaders")
|
||||
const linewrapEnabled = ref(false)
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
|
||||
@@ -355,7 +353,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -61,9 +61,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlQuery')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -112,8 +112,6 @@ 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)
|
||||
@@ -139,7 +137,7 @@ const prettifyQueryIcon = refAutoReset<
|
||||
typeof IconWand | typeof IconCheck | typeof IconInfo
|
||||
>(IconWand, 1000)
|
||||
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlQuery")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
||||
|
||||
@@ -186,7 +184,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "graphql",
|
||||
placeholder: `${t("request.query")}`,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: createGQLQueryLinter(schema),
|
||||
completer: queryCompleter(schema),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<div class="flex h-full flex-1 flex-col">
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky top-0 bg-primary z-10 border-b-0"
|
||||
|
||||
@@ -16,11 +16,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="
|
||||
toggleNestedSetting('WRAP_LINES', 'graphqlResponseBody')
|
||||
"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
@@ -72,9 +70,7 @@
|
||||
</tippy>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full">
|
||||
<div ref="schemaEditor"></div>
|
||||
</div>
|
||||
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
<component
|
||||
:is="response[0].error.component"
|
||||
@@ -103,8 +99,6 @@ 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,
|
||||
@@ -139,8 +133,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,
|
||||
@@ -149,7 +143,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -127,9 +127,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlSchema')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -145,9 +145,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="schemaString" class="h-full relative w-full">
|
||||
<div ref="schemaEditor" class="absolute inset-0"></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="schemaString"
|
||||
ref="schemaEditor"
|
||||
class="flex flex-1 flex-col"
|
||||
></div>
|
||||
<HoppSmartPlaceholder
|
||||
v-else
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
@@ -200,8 +202,6 @@ 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 WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlSchema")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
schemaEditor,
|
||||
@@ -358,7 +358,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "graphql",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'graphqlVariables')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -67,9 +67,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full relative">
|
||||
<div ref="variableEditor" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
<div ref="variableEditor" class="flex flex-1 flex-col"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -95,8 +93,6 @@ import {
|
||||
socketDisconnect,
|
||||
subscriptionState,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -118,7 +114,7 @@ const variableString = useVModel(props, "modelValue", emit)
|
||||
|
||||
const variableEditor = ref<any | null>(null)
|
||||
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlVariables")
|
||||
const linewrapEnabled = ref(false)
|
||||
|
||||
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
@@ -135,7 +131,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
placeholder: `${t("request.variables")}`,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: computed(() =>
|
||||
variableString.value.length > 0 ? jsonLinter : null
|
||||
|
||||
@@ -331,8 +331,7 @@ const deleteHistory = (entry: HistoryEntry) => {
|
||||
const addToCollection = (entry: HistoryEntry) => {
|
||||
if (props.page === "rest") {
|
||||
invokeAction("request.save-as", {
|
||||
requestType: "rest",
|
||||
request: entry.request as HoppRESTRequest,
|
||||
request: entry.request,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,17 @@
|
||||
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"
|
||||
@@ -43,17 +54,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<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: "inherit",
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +86,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'codeGen')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
@@ -161,8 +161,6 @@ 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()
|
||||
|
||||
@@ -189,8 +187,6 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
const requestCode = computed(() => {
|
||||
const aggregateEnvs = getAggregateEnvs()
|
||||
const env: Environment = {
|
||||
v: 1,
|
||||
id: "env",
|
||||
name: "Env",
|
||||
variables: aggregateEnvs,
|
||||
}
|
||||
@@ -226,7 +222,7 @@ const requestCode = computed(() => {
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const generatedCode = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "codeGen")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
generatedCode,
|
||||
@@ -235,7 +231,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
v-if="bulkMode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpHeaders')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -49,9 +49,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bulkMode" class="h-full relative w-full">
|
||||
<div ref="bulkEditor" class="absolute inset-0"></div>
|
||||
</div>
|
||||
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div>
|
||||
<div v-else>
|
||||
<draggable
|
||||
v-model="workingHeaders"
|
||||
@@ -334,8 +332,6 @@ 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()
|
||||
@@ -350,7 +346,7 @@ const idTicker = ref(0)
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
const bulkEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpHeaders")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||
|
||||
@@ -375,7 +371,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter,
|
||||
completer: null,
|
||||
@@ -557,7 +553,7 @@ const clearContent = () => {
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
||||
|
||||
const computedHeaders = computed(() =>
|
||||
getComputedHeaders(request.value, aggregateEnvs.value, false).map(
|
||||
getComputedHeaders(request.value, aggregateEnvs.value).map(
|
||||
(header, index) => ({
|
||||
id: `header-${index}`,
|
||||
...header,
|
||||
@@ -610,8 +606,7 @@ const inheritedProperties = computed(() => {
|
||||
const computedAuthHeader = getComputedAuthHeaders(
|
||||
aggregateEnvs.value,
|
||||
request.value,
|
||||
props.inheritedProperties.auth.inheritedAuth,
|
||||
false
|
||||
props.inheritedProperties.auth.inheritedAuth
|
||||
)[0]
|
||||
|
||||
if (
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'importCurl')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
@@ -96,8 +96,6 @@ 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()
|
||||
|
||||
@@ -108,7 +106,7 @@ const tabs = useService(RESTTabService)
|
||||
const curl = ref("")
|
||||
|
||||
const curlEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "importCurl")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const props = defineProps<{ show: boolean; text: string }>()
|
||||
|
||||
@@ -119,7 +117,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/x-sh",
|
||||
placeholder: `${t("request.enter_curl")}`,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -3,25 +3,14 @@
|
||||
<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"
|
||||
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||
></SmartEnvInput>
|
||||
<SmartEnvInput v-model="authURL" placeholder="Authorization URL" />
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="accessTokenURL"
|
||||
placeholder="Access Token URL"
|
||||
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||
/>
|
||||
<SmartEnvInput v-model="accessTokenURL" placeholder="Access Token URL" />
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
||||
@@ -55,7 +44,6 @@ 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()
|
||||
@@ -78,16 +66,10 @@ 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")
|
||||
|
||||
@@ -106,11 +88,13 @@ 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': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpParams')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -44,9 +44,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bulkMode" class="h-full relative">
|
||||
<div ref="bulkEditor" class="absolute inset-0"></div>
|
||||
</div>
|
||||
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div>
|
||||
<div v-else>
|
||||
<draggable
|
||||
v-model="workingParams"
|
||||
@@ -207,8 +205,6 @@ 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()
|
||||
|
||||
@@ -221,7 +217,7 @@ const idTicker = ref(0)
|
||||
const bulkMode = ref(false)
|
||||
const bulkParams = ref("")
|
||||
const bulkEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpParams")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||
|
||||
@@ -232,7 +228,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter,
|
||||
completer: null,
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpPreRequest')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight h-full relative">
|
||||
<div ref="preRequestEditor" class="h-full absolute inset-0"></div>
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div ref="preRequestEditor" class="h-full"></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,8 +72,6 @@ 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()
|
||||
|
||||
@@ -87,7 +85,7 @@ const emit = defineEmits<{
|
||||
const preRequestScript = useVModel(props, "modelValue", emit)
|
||||
|
||||
const preRequestEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpPreRequest")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
preRequestEditor,
|
||||
@@ -95,7 +93,7 @@ useCodemirror(
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
placeholder: `${t("preRequest.javascript_code")}`,
|
||||
},
|
||||
linter,
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpRequestBody')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="
|
||||
@@ -59,9 +59,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full relative">
|
||||
<div ref="rawBodyParameters" class="absolute inset-0"></div>
|
||||
</div>
|
||||
<div ref="rawBodyParameters" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -87,8 +85,6 @@ 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,
|
||||
@@ -126,7 +122,7 @@ const langLinter = computed(() =>
|
||||
isJSONContentType(body.value.contentType) ? jsonLinter : null
|
||||
)
|
||||
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpRequestBody")
|
||||
const linewrapEnabled = ref(true)
|
||||
const rawBodyParameters = ref<any | null>(null)
|
||||
|
||||
const codemirrorValue: Ref<string | undefined> =
|
||||
@@ -152,7 +148,7 @@ useCodemirror(
|
||||
codemirrorValue,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
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 { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
@@ -263,7 +263,6 @@ 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)
|
||||
@@ -327,8 +326,6 @@ 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")}`)
|
||||
@@ -344,7 +341,6 @@ const newSendRequest = async () => {
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "rest",
|
||||
strategy: interceptorService.currentInterceptorID.value!,
|
||||
workspaceType: workspaceService.currentWorkspace.value.type,
|
||||
})
|
||||
|
||||
const [cancel, streamPromise] = runRESTRequest$(tab)
|
||||
@@ -399,14 +395,17 @@ const newSendRequest = async () => {
|
||||
}
|
||||
|
||||
const ensureMethodInEndpoint = () => {
|
||||
const endpoint = newEndpoint.value.trim()
|
||||
tab.value.document.request.endpoint = endpoint
|
||||
if (!/^http[s]?:\/\//.test(endpoint) && !endpoint.startsWith("<<")) {
|
||||
const domain = endpoint.split(/[/:#?]+/)[0]
|
||||
if (
|
||||
!/^http[s]?:\/\//.test(newEndpoint.value) &&
|
||||
!newEndpoint.value.startsWith("<<")
|
||||
) {
|
||||
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
|
||||
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
|
||||
tab.value.document.request.endpoint = "http://" + endpoint
|
||||
tab.value.document.request.endpoint =
|
||||
"http://" + tab.value.document.request.endpoint
|
||||
} else {
|
||||
tab.value.document.request.endpoint = "https://" + endpoint
|
||||
tab.value.document.request.endpoint =
|
||||
"https://" + tab.value.document.request.endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,12 +577,25 @@ defineActionHandler("request.share-request", shareRequest)
|
||||
defineActionHandler("request.method.next", cycleDownMethod)
|
||||
defineActionHandler("request.method.prev", cycleUpMethod)
|
||||
defineActionHandler("request.save", saveRequest)
|
||||
defineActionHandler("request.save-as", (req) => {
|
||||
showSaveRequestModal.value = true
|
||||
if (req?.requestType === "rest") {
|
||||
request.value = req.request
|
||||
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.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" :is-embed="false" />
|
||||
<HttpResponse v-model:document="tab.document" />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
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()"
|
||||
@@ -59,18 +58,6 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<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"
|
||||
@@ -112,7 +99,6 @@ 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"
|
||||
|
||||
@@ -128,7 +114,6 @@ 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)
|
||||
@@ -138,5 +123,4 @@ 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,6 +211,7 @@ import { useI18n } from "@composables/i18n"
|
||||
import {
|
||||
globalEnv$,
|
||||
selectedEnvironmentIndex$,
|
||||
setGlobalEnvVariables,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
@@ -224,7 +225,6 @@ 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,10 +304,9 @@ const globalHasAdditions = computed(() => {
|
||||
|
||||
const addEnvToGlobal = () => {
|
||||
if (!testResults.value?.envDiff.selected.additions) return
|
||||
|
||||
invokeAction("modals.global.environment.update", {
|
||||
variables: testResults.value.envDiff.selected.additions,
|
||||
isSecret: false,
|
||||
})
|
||||
setGlobalEnvVariables([
|
||||
...globalEnvVars.value,
|
||||
...testResults.value.envDiff.selected.additions,
|
||||
])
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpTest')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight h-full relative">
|
||||
<div ref="testScriptEditor" class="h-full absolute inset-0"></div>
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div ref="testScriptEditor" class="h-full"></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,8 +72,6 @@ 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()
|
||||
|
||||
@@ -83,7 +81,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const testScript = useVModel(props, "modelValue", emit)
|
||||
const testScriptEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpTest")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
testScriptEditor,
|
||||
@@ -91,7 +89,7 @@ useCodemirror(
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
placeholder: `${t("test.javascript_code")}`,
|
||||
},
|
||||
linter,
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
v-if="bulkMode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpUrlEncoded')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -44,9 +44,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bulkMode" class="h-full relative">
|
||||
<div ref="bulkEditor" class="absolute inset-0"></div>
|
||||
</div>
|
||||
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div>
|
||||
<div v-else>
|
||||
<draggable
|
||||
v-model="workingUrlEncodedParams"
|
||||
@@ -198,8 +196,6 @@ 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"
|
||||
@@ -224,7 +220,7 @@ const idTicker = ref(0)
|
||||
const bulkMode = ref(false)
|
||||
const bulkUrlEncodedParams = ref("")
|
||||
const bulkEditor = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpUrlEncoded")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||
|
||||
@@ -235,7 +231,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter,
|
||||
completer: null,
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
@@ -44,9 +44,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="!previewEnabled" class="h-full">
|
||||
<div ref="htmlResponse" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
<div
|
||||
v-show="!previewEnabled"
|
||||
ref="htmlResponse"
|
||||
class="flex flex-1 flex-col"
|
||||
></div>
|
||||
<iframe
|
||||
v-show="previewEnabled"
|
||||
ref="previewFrame"
|
||||
@@ -74,8 +76,6 @@ 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 WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const { responseBodyText } = useResponseBody(props.response)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
@@ -104,7 +104,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "htmlmixed",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
@@ -119,12 +119,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full">
|
||||
<div
|
||||
ref="jsonResponse"
|
||||
:class="toggleFilter ? 'responseToggleOn' : 'responseToggleOff'"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
ref="jsonResponse"
|
||||
class="flex h-auto h-full flex-1 flex-col"
|
||||
:class="toggleFilter ? 'responseToggleOn' : 'responseToggleOff'"
|
||||
></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"
|
||||
@@ -261,8 +260,6 @@ 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()
|
||||
@@ -374,8 +371,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,
|
||||
@@ -384,7 +381,7 @@ const { cursor } = useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
@@ -35,9 +35,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full">
|
||||
<div ref="rawResponse" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
<div ref="rawResponse" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,8 +58,6 @@ 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()
|
||||
|
||||
@@ -101,7 +97,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
|
||||
|
||||
const rawResponse = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
rawResponse,
|
||||
@@ -110,7 +106,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'httpResponseBody')"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
@@ -35,9 +35,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full">
|
||||
<div ref="xmlResponse" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
<div ref="xmlResponse" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,8 +58,6 @@ 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()
|
||||
|
||||
@@ -95,7 +91,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
const { copyIcon, copyResponse } = useCopyResponse(responseBodyText)
|
||||
|
||||
const xmlResponse = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpResponseBody")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
xmlResponse,
|
||||
@@ -104,7 +100,7 @@ useCodemirror(
|
||||
extendedEditorConfig: {
|
||||
mode: "application/xml",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
|
||||
@@ -130,9 +130,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full">
|
||||
<div ref="wsCommunicationBody" class="flex flex-1 flex-col"></div>
|
||||
</div>
|
||||
<div ref="wsCommunicationBody" class="flex flex-1 flex-col"></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="customizeSharedRequest()"
|
||||
@click="openInNewTab"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
|
||||
@@ -62,7 +62,7 @@
|
||||
:shortcut="['T']"
|
||||
@click="
|
||||
() => {
|
||||
emit('open-shared-request', parseRequest)
|
||||
openInNewTab()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@@ -128,7 +128,7 @@ const emit = defineEmits<{
|
||||
embedProperties?: string | null
|
||||
): void
|
||||
(e: "delete-shared-request", codeID: string): void
|
||||
(e: "open-shared-request", request: HoppRESTRequest): void
|
||||
(e: "open-new-tab", request: HoppRESTRequest): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
@@ -145,6 +145,10 @@ const requestLabelColor = computed(() =>
|
||||
getMethodLabelColorClassOf(parseRequest.value)
|
||||
)
|
||||
|
||||
const openInNewTab = () => {
|
||||
emit("open-new-tab", parseRequest.value)
|
||||
}
|
||||
|
||||
const customizeSharedRequest = () => {
|
||||
const embedProperties = props.request.properties
|
||||
emit(
|
||||
|
||||
@@ -9,14 +9,8 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
|
||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-end 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"
|
||||
@@ -53,7 +47,7 @@
|
||||
:request="request"
|
||||
@customize-shared-request="customizeSharedRequest"
|
||||
@delete-shared-request="deleteSharedRequest"
|
||||
@open-shared-request="openRequestInNewTab"
|
||||
@open-new-tab="openInNewTab"
|
||||
/>
|
||||
<HoppSmartIntersection
|
||||
v-if="hasMoreSharedRequests"
|
||||
@@ -76,15 +70,7 @@
|
||||
: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
|
||||
@@ -109,7 +95,6 @@
|
||||
|
||||
<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"
|
||||
@@ -285,17 +270,6 @@ 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
|
||||
@@ -460,6 +434,13 @@ 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 {
|
||||
@@ -484,13 +465,6 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
}
|
||||
}
|
||||
|
||||
const openRequestInNewTab = (request: HoppRESTRequest) => {
|
||||
restTab.createNewTab({
|
||||
isDirty: false,
|
||||
request,
|
||||
})
|
||||
}
|
||||
|
||||
defineActionHandler("share.request", ({ request }) => {
|
||||
requestToShare.value = request
|
||||
displayShareRequestModal(true)
|
||||
|
||||
@@ -3,18 +3,7 @@
|
||||
<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"
|
||||
@@ -22,14 +11,7 @@
|
||||
@click="emit('click', $event)"
|
||||
@keydown="handleKeystroke"
|
||||
@focusin="showSuggestionPopover = true"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="secret"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="isSecret ? t('action.show_secret') : t('action.hide_secret')"
|
||||
:icon="isSecret ? IconEyeoff : IconEye"
|
||||
@click="toggleSecret"
|
||||
/>
|
||||
></div>
|
||||
<AppInspection
|
||||
:inspection-results="inspectionResults"
|
||||
class="sticky inset-y-0 right-0 rounded-r bg-primary"
|
||||
@@ -79,29 +61,18 @@ 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,
|
||||
aggregateEnvsWithSecrets$,
|
||||
} from "~/newstore/environments"
|
||||
import { AggregateEnvironment, aggregateEnvs$ } 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?: Env[] | null
|
||||
envs?: { key: string; value: string; source: string }[] | null
|
||||
focus?: boolean
|
||||
selectTextOnMount?: boolean
|
||||
environmentHighlights?: boolean
|
||||
@@ -109,7 +80,6 @@ const props = withDefaults(
|
||||
autoCompleteSource?: string[]
|
||||
inspectionResults?: InspectorResult[] | undefined
|
||||
contextMenuEnabled?: boolean
|
||||
secret?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
@@ -123,7 +93,6 @@ const props = withDefaults(
|
||||
inspectionResult: undefined,
|
||||
inspectionResults: undefined,
|
||||
contextMenuEnabled: true,
|
||||
secret: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -149,27 +118,10 @@ 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) {
|
||||
@@ -217,6 +169,8 @@ watch(
|
||||
)
|
||||
|
||||
const handleKeystroke = (ev: KeyboardEvent) => {
|
||||
if (!props.autoCompleteSource) return
|
||||
|
||||
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(ev.key)) {
|
||||
ev.preventDefault()
|
||||
}
|
||||
@@ -353,28 +307,19 @@ watch(
|
||||
let clipboardEv: ClipboardEvent | null = null
|
||||
let pastedValue: string | null = null
|
||||
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
|
||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
|
||||
AggregateEnvironment[]
|
||||
>
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
const envVars = computed(() =>
|
||||
props.envs
|
||||
? props.envs.map((x) => ({
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: x.source,
|
||||
}))
|
||||
: aggregateEnvs.value
|
||||
})
|
||||
)
|
||||
|
||||
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
||||
|
||||
@@ -418,28 +363,17 @@ 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 (readonly) {
|
||||
if (props.readonly) {
|
||||
update.view.contentDOM.inputMode = "none"
|
||||
}
|
||||
}),
|
||||
EditorState.changeFilter.of(() => !readonly),
|
||||
EditorState.changeFilter.of(() => !props.readonly),
|
||||
inputTheme,
|
||||
readonly
|
||||
props.readonly
|
||||
? EditorView.theme({
|
||||
".cm-content": {
|
||||
caretColor: "var(--secondary-dark-color)",
|
||||
@@ -450,7 +384,6 @@ const getExtensions = (readonly: boolean): Extension => {
|
||||
})
|
||||
: EditorView.theme({}),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
position: "absolute",
|
||||
}),
|
||||
props.environmentHighlights ? envTooltipPlugin : [],
|
||||
@@ -472,8 +405,7 @@ const getExtensions = (readonly: boolean): Extension => {
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
if (readonly) return
|
||||
|
||||
if (props.readonly) return
|
||||
if (update.docChanged) {
|
||||
const prevValue = clone(cachedValue.value)
|
||||
|
||||
@@ -522,7 +454,14 @@ const getExtensions = (readonly: boolean): Extension => {
|
||||
history(),
|
||||
keymap.of([...historyKeymap]),
|
||||
]
|
||||
return extensions
|
||||
|
||||
view.value = new EditorView({
|
||||
parent: el,
|
||||
state: EditorState.create({
|
||||
doc: props.modelValue,
|
||||
extensions,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const triggerTextSelection = () => {
|
||||
@@ -535,11 +474,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,7 +4,6 @@ import {
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
placeholder,
|
||||
tooltips,
|
||||
} from "@codemirror/view"
|
||||
import {
|
||||
Extension,
|
||||
@@ -270,7 +269,6 @@ export function useCodemirror(
|
||||
basicSetup,
|
||||
baseTheme,
|
||||
syntaxHighlighting(baseHighlightStyle, { fallback: true }),
|
||||
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
@@ -320,7 +318,6 @@ export function useCodemirror(
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
EditorView.domEventHandlers({
|
||||
scroll(event) {
|
||||
if (event.target && options.contextMenuEnabled) {
|
||||
@@ -362,10 +359,6 @@ export function useCodemirror(
|
||||
run: indentLess,
|
||||
},
|
||||
]),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
position: "absolute",
|
||||
}),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
additionalExts.of(options.additionalExts ?? []),
|
||||
]
|
||||
|
||||
@@ -13,36 +13,7 @@ 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,
|
||||
},
|
||||
})
|
||||
@@ -64,9 +35,7 @@ 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,13 +30,6 @@ 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" }
|
||||
@@ -65,63 +58,15 @@ const getTestableBody = (
|
||||
return x
|
||||
}
|
||||
|
||||
const combineEnvVariables = (envs: {
|
||||
const combineEnvVariables = (env: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}) => [...envs.selected, ...envs.global]
|
||||
}) => [...env.selected, ...env.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>>
|
||||
): [
|
||||
@@ -209,36 +154,15 @@ 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
|
||||
|
||||
const updatedRunResult = {
|
||||
...runResult.right,
|
||||
envs: {
|
||||
global: updatedGlobalEnvVariables,
|
||||
selected: updatedSelectedEnvVariables,
|
||||
},
|
||||
}
|
||||
|
||||
tab.value.document.testResults =
|
||||
translateToSandboxTestResults(updatedRunResult)
|
||||
|
||||
setGlobalEnvVariables(
|
||||
updateEnvironmentsWithSecret(
|
||||
runResult.right.envs.global,
|
||||
"global"
|
||||
)
|
||||
tab.value.document.testResults = translateToSandboxTestResults(
|
||||
runResult.right
|
||||
)
|
||||
|
||||
setGlobalEnvVariables(runResult.right.envs.global)
|
||||
|
||||
if (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
|
||||
) {
|
||||
@@ -249,10 +173,8 @@ export function runRESTRequest$(
|
||||
updateEnvironment(
|
||||
environmentsStore.value.selectedEnvironmentIndex.index,
|
||||
{
|
||||
name: env.name,
|
||||
v: 1,
|
||||
id: env.id ?? "",
|
||||
variables: updatedRunResult.envs.selected,
|
||||
...env,
|
||||
variables: runResult.right.envs.selected,
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
@@ -264,7 +186,7 @@ export function runRESTRequest$(
|
||||
})
|
||||
pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(updatedRunResult.envs.selected),
|
||||
JSON.stringify(runResult.right.envs.selected),
|
||||
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
|
||||
env.name
|
||||
)
|
||||
@@ -353,6 +275,7 @@ 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 { Environment, HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { 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,7 +43,6 @@ 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
|
||||
@@ -67,13 +66,6 @@ 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
|
||||
@@ -94,19 +86,13 @@ 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
|
||||
@@ -126,7 +112,6 @@ type HoppActionArgsMap = {
|
||||
requestType: "gql"
|
||||
request: HoppGQLRequest
|
||||
}
|
||||
| undefined
|
||||
"request.open-tab": {
|
||||
tab: RESTOptionTabs | GQLOptionTabs
|
||||
}
|
||||
@@ -136,6 +121,7 @@ type HoppActionArgsMap = {
|
||||
"tab.duplicate-tab": {
|
||||
tabID?: string
|
||||
}
|
||||
|
||||
"gql.request.open": {
|
||||
request: HoppGQLRequest
|
||||
saveContext?: HoppGQLSaveContext
|
||||
@@ -146,23 +132,11 @@ 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
|
||||
*/
|
||||
@@ -171,26 +145,27 @@ export type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
|
||||
/**
|
||||
* Resolves the argument type for a given HoppAction
|
||||
*/
|
||||
type ArgOfHoppAction<A extends HoppAction> = A extends HoppActionWithArgs
|
||||
? HoppActionArgsMap[A]
|
||||
: undefined
|
||||
type ArgOfHoppAction<A extends HoppAction | HoppActionWithArgs> =
|
||||
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> = A extends HoppActionWithArgs
|
||||
? (arg: ArgOfHoppAction<A>, trigger?: InvocationTriggers) => void
|
||||
: (_?: undefined, trigger?: InvocationTriggers) => void
|
||||
type ActionFunc<A extends HoppAction | HoppActionWithArgs> =
|
||||
A extends HoppActionWithArgs ? (arg: ArgOfHoppAction<A>) => void : () => void
|
||||
|
||||
type BoundActionList = {
|
||||
[A in HoppAction]?: Array<ActionFunc<A>>
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[A in HoppAction | HoppActionWithArgs]?: Array<ActionFunc<A>>
|
||||
}
|
||||
|
||||
const boundActions: BoundActionList = reactive({})
|
||||
|
||||
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
|
||||
export const activeActions$ = new BehaviorSubject<
|
||||
(HoppAction | HoppActionWithArgs)[]
|
||||
>([])
|
||||
|
||||
export function bindAction<A extends HoppAction>(
|
||||
export function bindAction<A extends HoppAction | HoppActionWithArgs>(
|
||||
action: A,
|
||||
handler: ActionFunc<A>
|
||||
) {
|
||||
@@ -204,33 +179,27 @@ export function bindAction<A extends HoppAction>(
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
export type InvocationTriggers = "keypress" | "mouseclick"
|
||||
|
||||
type InvokeActionFunc = {
|
||||
(
|
||||
action: HoppActionWithOptionalArgs,
|
||||
args?: undefined,
|
||||
trigger?: InvocationTriggers
|
||||
): void
|
||||
(action: HoppActionWithNoArgs, args?: undefined): void
|
||||
<A extends HoppActionWithArgs>(action: A, args: HoppActionArgsMap[A]): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes an action, triggering action handlers if any registered.
|
||||
* The second and third arguments are optional
|
||||
* Invokes a action, triggering action handlers if any registered.
|
||||
* The second argument parameter is optional if your action has no args required
|
||||
* @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>(
|
||||
export const invokeAction: InvokeActionFunc = <
|
||||
A extends HoppAction | HoppActionWithArgs,
|
||||
>(
|
||||
action: A,
|
||||
args?: ArgOfHoppAction<A>,
|
||||
trigger?: InvocationTriggers
|
||||
args: ArgOfHoppAction<A>
|
||||
) => {
|
||||
boundActions[action]?.forEach((handler) => handler(args! as any, trigger))
|
||||
boundActions[action]?.forEach((handler) => handler(args! as any))
|
||||
}
|
||||
|
||||
export function unbindAction<A extends HoppAction>(
|
||||
export function unbindAction<A extends HoppAction | HoppActionWithArgs>(
|
||||
action: A,
|
||||
handler: ActionFunc<A>
|
||||
) {
|
||||
@@ -263,7 +232,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>(
|
||||
export function defineActionHandler<A extends HoppAction | HoppActionWithArgs>(
|
||||
action: A,
|
||||
handler: ActionFunc<A>,
|
||||
isActive: Ref<boolean> | undefined = undefined
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
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: "inherit", authActive: true },
|
||||
auth: { authType: "none", 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: "inherit", authActive: true },
|
||||
auth: { authType: "none", 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: "inherit", authActive: true },
|
||||
auth: { authType: "none", 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: "inherit", authActive: true },
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
@@ -217,7 +217,7 @@ const samples = [
|
||||
method: "POST",
|
||||
name: "Untitled",
|
||||
endpoint: "https://bing.com/",
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
contentType: "multipart/form-data",
|
||||
body: [
|
||||
@@ -301,7 +301,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost:9900/",
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
@@ -345,7 +345,7 @@ const samples = [
|
||||
endpoint: "https://hoppscotch.io/?io",
|
||||
auth: {
|
||||
authActive: true,
|
||||
authType: "inherit",
|
||||
authType: "none",
|
||||
},
|
||||
body: {
|
||||
contentType: null,
|
||||
@@ -380,7 +380,7 @@ const samples = [
|
||||
endpoint: "https://someshadywebsite.com/questionable/path/?so",
|
||||
auth: {
|
||||
authActive: true,
|
||||
authType: "inherit",
|
||||
authType: "none",
|
||||
},
|
||||
body: {
|
||||
contentType: "multipart/form-data",
|
||||
@@ -441,7 +441,7 @@ const samples = [
|
||||
endpoint: "http://localhost/",
|
||||
auth: {
|
||||
authActive: true,
|
||||
authType: "inherit",
|
||||
authType: "none",
|
||||
},
|
||||
body: {
|
||||
contentType: "multipart/form-data",
|
||||
@@ -473,7 +473,7 @@ const samples = [
|
||||
method: "GET",
|
||||
name: "Untitled",
|
||||
endpoint: "https://hoppscotch.io/",
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
@@ -528,7 +528,7 @@ const samples = [
|
||||
method: "GET",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
auth: { authType: "none", 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: "inherit", authActive: true },
|
||||
auth: { authType: "none", authActive: true },
|
||||
headers: [
|
||||
{
|
||||
active: true,
|
||||
@@ -615,7 +615,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://muxueqz.top/skybook.html",
|
||||
method: "GET",
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
auth: { authType: "none", 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: "inherit", authActive: true },
|
||||
auth: { authType: "none", 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: "inherit", authActive: true },
|
||||
auth: { authType: "none", authActive: true },
|
||||
headers: [],
|
||||
body: {
|
||||
contentType: null,
|
||||
@@ -670,7 +670,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "GET",
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
auth: { authType: "none", authActive: true },
|
||||
headers: [
|
||||
{
|
||||
active: true,
|
||||
@@ -693,7 +693,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "GET",
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
auth: { authType: "none", authActive: true },
|
||||
headers: [],
|
||||
body: {
|
||||
contentType: null,
|
||||
@@ -710,7 +710,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://example.org/",
|
||||
method: "HEAD",
|
||||
auth: { authType: "inherit", authActive: true },
|
||||
auth: { authType: "none", authActive: true },
|
||||
headers: [],
|
||||
body: {
|
||||
contentType: null,
|
||||
@@ -756,7 +756,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
@@ -777,7 +777,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
@@ -797,7 +797,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "http://192.168.0.24:8080/ping",
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
@@ -817,7 +817,7 @@ const samples = [
|
||||
name: "Untitled",
|
||||
endpoint: "https://example.com/",
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
|
||||
@@ -12,17 +12,14 @@ import { parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { StreamSubscriberFunc } from "@composables/stream"
|
||||
import {
|
||||
AggregateEnvironment,
|
||||
aggregateEnvsWithSecrets$,
|
||||
getAggregateEnvsWithSecrets,
|
||||
getCurrentEnvironment,
|
||||
aggregateEnvs$,
|
||||
getAggregateEnvs,
|
||||
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
|
||||
|
||||
@@ -31,8 +28,6 @@ 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) => {
|
||||
@@ -71,27 +66,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
|
||||
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
|
||||
|
||||
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 envValue = tooltipEnv?.value ?? "Not found"
|
||||
|
||||
const result = parseTemplateStringE(envValue, aggregateEnvs)
|
||||
|
||||
@@ -108,25 +83,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
editIcon.className =
|
||||
"ml-2 cursor-pointer text-accent hover:text-accentDark"
|
||||
editIcon.addEventListener("click", () => {
|
||||
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",
|
||||
const isPersonalEnv =
|
||||
envName === "Global" || selectedEnvType !== "TEAM_ENV"
|
||||
const action = isPersonalEnv ? "my" : "team"
|
||||
invokeAction(`modals.${action}.environment.edit`, {
|
||||
envName,
|
||||
variableName: parsedEnvKey,
|
||||
isSecret: tooltipEnv?.secret,
|
||||
})
|
||||
})
|
||||
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
||||
@@ -209,10 +171,11 @@ export class HoppEnvironmentPlugin {
|
||||
subscribeToStream: StreamSubscriberFunc,
|
||||
private editorView: Ref<EditorView | undefined>
|
||||
) {
|
||||
this.envs = getAggregateEnvsWithSecrets()
|
||||
this.envs = getAggregateEnvs()
|
||||
|
||||
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
|
||||
subscribeToStream(aggregateEnvs$, (envs) => {
|
||||
this.envs = envs
|
||||
|
||||
this.editorView.value?.dispatch({
|
||||
effects: this.compartment.reconfigure([
|
||||
cursorTooltipField(this.envs),
|
||||
|
||||
@@ -171,6 +171,9 @@ 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)",
|
||||
@@ -317,6 +320,9 @@ 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: "inherit",
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -12,6 +12,8 @@ const getEnvironmentJson = (
|
||||
? cloneDeep(environmentObj.environment)
|
||||
: cloneDeep(environmentObj)
|
||||
|
||||
delete newEnvironment.id
|
||||
|
||||
const environmentId =
|
||||
environmentIndex || environmentIndex === 0
|
||||
? environmentIndex
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { entityReference } from "verzod"
|
||||
import * as O from "fp-ts/Option"
|
||||
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
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)
|
||||
|
||||
@@ -16,9 +25,7 @@ export const hoppEnvImporter = (content: string) => {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const validationResult = z
|
||||
.array(entityReference(Environment))
|
||||
.safeParse(parsedContent.value)
|
||||
const validationResult = z.array(hoppEnvSchema).safeParse(parsedContent.value)
|
||||
|
||||
if (!validationResult.success) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
|
||||
@@ -4,9 +4,8 @@ import * as O from "fp-ts/Option"
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
import { z } from "zod"
|
||||
import { NonSecretEnvironment } from "@hoppscotch/data"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
const insomniaResourcesSchema = z.object({
|
||||
resources: z.array(
|
||||
@@ -57,18 +56,16 @@ export const insomniaEnvImporter = (content: string) => {
|
||||
return { ...envResource, data: stringifiedData }
|
||||
})
|
||||
|
||||
const environments: NonSecretEnvironment[] = []
|
||||
const environments: Environment[] = []
|
||||
|
||||
insomniaEnvs.forEach((insomniaEnv) => {
|
||||
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
|
||||
|
||||
if (parsedInsomniaEnv.success) {
|
||||
const environment: NonSecretEnvironment = {
|
||||
id: uniqueId(),
|
||||
v: 1,
|
||||
const environment: Environment = {
|
||||
name: parsedInsomniaEnv.data.name,
|
||||
variables: Object.entries(parsedInsomniaEnv.data.data).map(
|
||||
([key, value]) => ({ key, value, secret: false })
|
||||
([key, value]) => ({ key, value })
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ 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(),
|
||||
@@ -35,14 +34,12 @@ 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, secret: false })
|
||||
environment.variables.push({ key, value })
|
||||
)
|
||||
|
||||
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