Compare commits

..

8 Commits

Author SHA1 Message Date
nivedin
9581f1d747 refactor: asterik length same as real text length 2023-09-21 13:19:57 +05:30
nivedin
882aafdb43 refactor: update secret toggle icon 2023-09-21 13:18:50 +05:30
Daniel Maurer
f33fa6ac1a fix: adjusted tests to work with the secret flag 2023-09-19 12:37:31 +05:30
Daniel Maurer
b9a1cc21f1 fix: fixed bug in gui with masked secrets 2023-09-19 12:37:31 +05:30
Daniel Maurer
f530fc2853 fix: inputfield to readonly + asterisk if secret 2023-09-19 12:37:28 +05:30
Daniel Maurer
e0eb8af6f5 feat: added masking of secrets in cli in url 2023-09-19 12:37:13 +05:30
Daniel Maurer
088f1d6b47 feat: missing files from last commit 2023-09-19 12:37:13 +05:30
Daniel Maurer
7d61e69b3d feat: added toggle Button to Frontend 2023-09-19 12:37:10 +05:30
334 changed files with 5222 additions and 19438 deletions

View File

@@ -5,5 +5,5 @@
"features": { "features": {
"ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {} "ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {}
}, },
"postCreateCommand": "cp .env.example .env && pnpm i" "postCreateCommand": "mv .env.example .env && pnpm i"
} }

View File

@@ -18,9 +18,6 @@ jobs:
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:

View File

@@ -36,7 +36,7 @@ jobs:
# Deploy the ui site with netlify-cli # Deploy the ui site with netlify-cli
- name: Deploy to Netlify (ui) - name: Deploy to Netlify (ui)
run: npx netlify-cli@15.11.0 deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
env: env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -22,14 +22,16 @@
"workspaces": [ "workspaces": [
"./packages/*" "./packages/*"
], ],
"dependencies": {
"husky": "^7.0.4",
"lint-staged": "^12.3.8"
},
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^16.2.3", "@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1", "@commitlint/config-conventional": "^16.2.1",
"@types/node": "17.0.27", "@types/node": "^17.0.24",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"http-server": "^14.1.1", "http-server": "^14.1.1"
"husky": "^7.0.4",
"lint-staged": "12.4.0"
}, },
"pnpm": { "pnpm": {
"packageExtensions": { "packageExtensions": {

View File

@@ -17,16 +17,16 @@
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@codemirror/language": "6.9.0", "@codemirror/language": "^6.9.0",
"@lezer/highlight": "1.1.4", "@lezer/highlight": "^1.1.6",
"@lezer/lr": "^1.3.13" "@lezer/lr": "^1.3.10"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.5.1", "@lezer/generator": "^1.5.0",
"mocha": "^9.2.2", "mocha": "^9.2.2",
"rollup": "^3.29.3", "rollup": "^2.70.2",
"rollup-plugin-dts": "^6.0.2", "rollup-plugin-dts": "^4.2.1",
"rollup-plugin-ts": "^3.4.5", "rollup-plugin-ts": "^2.0.7",
"typescript": "^5.2.2" "typescript": "^4.6.3"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoppscotch-backend", "name": "hoppscotch-backend",
"version": "2023.8.3-1", "version": "2023.8.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -24,17 +24,18 @@
"do-test": "pnpm run test" "do-test": "pnpm run test"
}, },
"dependencies": { "dependencies": {
"@apollo/server": "^4.9.4", "@nestjs-modules/mailer": "^1.8.1",
"@nestjs-modules/mailer": "^1.9.1", "@nestjs/apollo": "^10.1.6",
"@nestjs/apollo": "^12.0.9", "@nestjs/common": "^9.2.1",
"@nestjs/common": "^10.2.6", "@nestjs/core": "^9.2.1",
"@nestjs/core": "^10.2.6", "@nestjs/graphql": "^10.1.6",
"@nestjs/graphql": "^12.0.9", "@nestjs/jwt": "^10.0.1",
"@nestjs/jwt": "^10.1.1", "@nestjs/passport": "^9.0.0",
"@nestjs/passport": "^10.0.2", "@nestjs/platform-express": "^9.2.1",
"@nestjs/platform-express": "^10.2.6", "@nestjs/throttler": "^4.0.0",
"@nestjs/throttler": "^5.0.0",
"@prisma/client": "^4.16.2", "@prisma/client": "^4.16.2",
"apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"cookie": "^0.5.0", "cookie": "^0.5.0",
@@ -42,9 +43,9 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"fp-ts": "^2.13.1", "fp-ts": "^2.13.1",
"graphql": "^16.8.1", "graphql": "^15.5.0",
"graphql-query-complexity": "^0.12.0", "graphql-query-complexity": "^0.12.0",
"graphql-redis-subscriptions": "^2.6.0", "graphql-redis-subscriptions": "^2.5.0",
"graphql-subscriptions": "^2.0.0", "graphql-subscriptions": "^2.0.0",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"io-ts": "^2.2.16", "io-ts": "^2.2.16",
@@ -62,9 +63,9 @@
"rxjs": "^7.6.0" "rxjs": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.1.18", "@nestjs/cli": "^9.1.5",
"@nestjs/schematics": "^10.0.2", "@nestjs/schematics": "^9.0.3",
"@nestjs/testing": "^10.2.6", "@nestjs/testing": "^9.2.1",
"@relmify/jest-fp-ts": "^2.0.2", "@relmify/jest-fp-ts": "^2.0.2",
"@types/argon2": "^0.15.0", "@types/argon2": "^0.15.0",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",

View File

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

View File

@@ -27,7 +27,12 @@ import { AppController } from './app.controller';
buildSchemaOptions: { buildSchemaOptions: {
numberScalarMode: 'integer', numberScalarMode: 'integer',
}, },
cors: {
origin: process.env.WHITELISTED_ORIGINS.split(','),
credentials: true,
},
playground: process.env.PRODUCTION !== 'true', playground: process.env.PRODUCTION !== 'true',
debug: process.env.PRODUCTION !== 'true',
autoSchemaFile: true, autoSchemaFile: true,
installSubscriptionHandlers: true, installSubscriptionHandlers: true,
subscriptions: { subscriptions: {
@@ -57,12 +62,10 @@ import { AppController } from './app.controller';
}), }),
driver: ApolloDriver, driver: ApolloDriver,
}), }),
ThrottlerModule.forRoot([ ThrottlerModule.forRoot({
{ ttl: +process.env.RATE_LIMIT_TTL,
ttl: +process.env.RATE_LIMIT_TTL, limit: +process.env.RATE_LIMIT_MAX,
limit: +process.env.RATE_LIMIT_MAX, }),
},
]),
UserModule, UserModule,
AuthModule, AuthModule,
AdminModule, AdminModule,

View File

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

View File

@@ -93,7 +93,9 @@ export async function emitGQLSchemaFile() {
numberScalarMode: 'integer', numberScalarMode: 'integer',
}); });
const schemaString = printSchema(schema); const schemaString = printSchema(schema, {
commentDescriptions: true,
});
logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`); logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`);

View File

@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class ThrottlerBehindProxyGuard extends ThrottlerGuard { export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
protected async getTracker(req: Record<string, any>): Promise<string> { protected getTracker(req: Record<string, any>): string {
return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs
// learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#directives
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
import { GraphQLSchemaHost } from '@nestjs/graphql'; import { GraphQLSchemaHost } from '@nestjs/graphql';
import { import {
ApolloServerPlugin, ApolloServerPlugin,
BaseContext,
GraphQLRequestListener, GraphQLRequestListener,
} from '@apollo/server'; } from 'apollo-server-plugin-base';
import { Plugin } from '@nestjs/apollo'; import { Plugin } from '@nestjs/apollo';
import { GraphQLError } from 'graphql'; import { GraphQLError } from 'graphql';
import { import {
@@ -18,7 +17,7 @@ const COMPLEXITY_LIMIT = 50;
export class GQLComplexityPlugin implements ApolloServerPlugin { export class GQLComplexityPlugin implements ApolloServerPlugin {
constructor(private gqlSchemaHost: GraphQLSchemaHost) {} constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
async requestDidStart(): Promise<GraphQLRequestListener<BaseContext>> { async requestDidStart(): Promise<GraphQLRequestListener> {
const { schema } = this.gqlSchemaHost; const { schema } = this.gqlSchemaHost;
return { return {

View File

@@ -1,5 +1,5 @@
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client'; import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mock, mockDeep, mockReset } from 'jest-mock-extended';
import { import {
TEAM_COLL_DEST_SAME, TEAM_COLL_DEST_SAME,
TEAM_COLL_INVALID_JSON, TEAM_COLL_INVALID_JSON,
@@ -17,6 +17,9 @@ import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
import { TeamCollectionService } from './team-collection.service'; import { TeamCollectionService } from './team-collection.service';
import { TeamCollection } from './team-collection.model';
import { TeamCollectionModule } from './team-collection.module';
import * as E from 'fp-ts/Either';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>(); const mockPubSub = mockDeep<PubSubService>();

View File

@@ -301,7 +301,7 @@ describe('TeamEnvironmentsService', () => {
describe('createDuplicateEnvironment', () => { describe('createDuplicateEnvironment', () => {
test('should successfully duplicate an existing team environment', async () => { test('should successfully duplicate an existing team environment', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce( mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
teamEnvironment, teamEnvironment,
); );
@@ -322,9 +322,7 @@ describe('TeamEnvironmentsService', () => {
}); });
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValue( mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
'NotFoundError',
);
const result = await teamEnvironmentsService.createDuplicateEnvironment( const result = await teamEnvironmentsService.createDuplicateEnvironment(
teamEnvironment.id, teamEnvironment.id,
@@ -334,7 +332,7 @@ describe('TeamEnvironmentsService', () => {
}); });
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => { test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce( mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
teamEnvironment, teamEnvironment,
); );

View File

@@ -183,10 +183,11 @@ export class TeamEnvironmentsService {
*/ */
async createDuplicateEnvironment(id: string) { async createDuplicateEnvironment(id: string) {
try { try {
const environment = await this.prisma.teamEnvironment.findFirstOrThrow({ const environment = await this.prisma.teamEnvironment.findFirst({
where: { where: {
id: id, id: id,
}, },
rejectOnNotFound: true,
}); });
const result = await this.prisma.teamEnvironment.create({ const result = await this.prisma.teamEnvironment.create({

View File

@@ -142,15 +142,13 @@ describe('UserHistoryService', () => {
}); });
describe('createUserHistory', () => { describe('createUserHistory', () => {
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => { test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
const executedOn = new Date();
mockPrisma.userHistory.create.mockResolvedValueOnce({ mockPrisma.userHistory.create.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}); });
@@ -160,7 +158,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}; };
@@ -174,15 +172,13 @@ describe('UserHistoryService', () => {
).toEqualRight(userHistory); ).toEqualRight(userHistory);
}); });
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => { test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
const executedOn = new Date();
mockPrisma.userHistory.create.mockResolvedValueOnce({ mockPrisma.userHistory.create.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.GQL, reqType: ReqType.GQL,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}); });
@@ -192,7 +188,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.GQL, reqType: ReqType.GQL,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}; };
@@ -216,15 +212,13 @@ describe('UserHistoryService', () => {
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE); ).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
}); });
test('Should create a GQL request to users history and publish a created subscription', async () => { test('Should create a GQL request to users history and publish a created subscription', async () => {
const executedOn = new Date();
mockPrisma.userHistory.create.mockResolvedValueOnce({ mockPrisma.userHistory.create.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.GQL, reqType: ReqType.GQL,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}); });
@@ -234,7 +228,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.GQL, reqType: ReqType.GQL,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}; };
@@ -251,15 +245,13 @@ describe('UserHistoryService', () => {
); );
}); });
test('Should create a REST request to users history and publish a created subscription', async () => { test('Should create a REST request to users history and publish a created subscription', async () => {
const executedOn = new Date();
mockPrisma.userHistory.create.mockResolvedValueOnce({ mockPrisma.userHistory.create.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}); });
@@ -269,7 +261,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}; };
@@ -331,15 +323,13 @@ describe('UserHistoryService', () => {
).toEqualLeft(USER_HISTORY_NOT_FOUND); ).toEqualLeft(USER_HISTORY_NOT_FOUND);
}); });
test('Should star/unstar a request in the history and publish a updated subscription', async () => { test('Should star/unstar a request in the history and publish a updated subscription', async () => {
const executedOn = new Date();
mockPrisma.userHistory.findFirst.mockResolvedValueOnce({ mockPrisma.userHistory.findFirst.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn, executedOn: new Date(),
isStarred: false, isStarred: false,
}); });
@@ -349,7 +339,7 @@ describe('UserHistoryService', () => {
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn, executedOn: new Date(),
isStarred: true, isStarred: true,
}); });
@@ -359,7 +349,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn, executedOn: new Date(),
isStarred: true, isStarred: true,
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "@hoppscotch/cli", "name": "@hoppscotch/cli",
"version": "0.3.3", "version": "0.3.2",
"description": "A CLI to run Hoppscotch test scripts in CI environments.", "description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io", "homepage": "https://hoppscotch.io",
"main": "dist/index.js", "main": "dist/index.js",
@@ -40,6 +40,9 @@
"@hoppscotch/js-sandbox": "workspace:^", "@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "^2.0.2", "@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.2.181", "@swc/core": "^1.2.181",
"@types/axios": "^0.14.0",
"@types/chalk": "^2.2.0",
"@types/commander": "^2.12.2",
"@types/jest": "^27.4.1", "@types/jest": "^27.4.1",
"@types/lodash": "^4.14.181", "@types/lodash": "^4.14.181",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
@@ -55,7 +58,6 @@
"qs": "^6.10.3", "qs": "^6.10.3",
"ts-jest": "^27.1.4", "ts-jest": "^27.1.4",
"tsup": "^5.12.7", "tsup": "^5.12.7",
"typescript": "^4.6.4", "typescript": "^4.6.4"
"zod": "^3.22.2"
} }
} }

View File

@@ -14,9 +14,10 @@ import { isHoppCLIError } from "../utils/checks";
export const test = (path: string, options: TestCmdOptions) => async () => { export const test = (path: string, options: TestCmdOptions) => async () => {
try { try {
const delay = options.delay ? parseDelayOption(options.delay) : 0 const delay = options.delay ? parseDelayOption(options.delay) : 0
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] } const envName = options.envName
const envs = options.env ? await parseEnvsData(options.env, envName) : <HoppEnvs>{ global: [], selected: [] }
const collections = await parseCollectionData(path) const collections = await parseCollectionData(path)
const report = await collectionsRunner({collections, envs, delay}) const report = await collectionsRunner({collections, envs, delay})
const hasSucceeded = collectionsRunnerResult(report) const hasSucceeded = collectionsRunnerResult(report)
collectionsRunnerExit(hasSucceeded) collectionsRunnerExit(hasSucceeded)

View File

@@ -48,14 +48,12 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
ERROR_MSG = `Unavailable command: ${error.command}`; ERROR_MSG = `Unavailable command: ${error.command}`;
break; break;
case "MALFORMED_ENV_FILE": case "MALFORMED_ENV_FILE":
ERROR_MSG = `The environment file is not of the correct format.`;
break;
case "BULK_ENV_FILE":
ERROR_MSG = `CLI doesn't support bulk environments export.`;
break;
case "MALFORMED_COLLECTION": case "MALFORMED_COLLECTION":
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`; ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
break; break;
case "ENVIRONMENT_NAME_NOT_FOUND":
ERROR_MSG = `\n${parseErrorData(error.data)}`;
break;
case "NO_FILE_PATH": case "NO_FILE_PATH":
ERROR_MSG = `Please provide a hoppscotch-collection file path.`; ERROR_MSG = `Please provide a hoppscotch-collection file path.`;
break; break;
@@ -87,4 +85,4 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
if (!S.isEmpty(ERROR_MSG)) { if (!S.isEmpty(ERROR_MSG)) {
console.error(ERROR_CODE, ERROR_MSG); console.error(ERROR_CODE, ERROR_MSG);
} }
}; };

View File

@@ -50,6 +50,7 @@ program
"path to a hoppscotch collection.json file for CI testing" "path to a hoppscotch collection.json file for CI testing"
) )
.option("-e, --env <file_path>", "path to an environment variables json file") .option("-e, --env <file_path>", "path to an environment variables json file")
.option("-eN, --envName <environment_name>","Specific Name of the environment")
.option( .option(
"-d, --delay <delay_in_ms>", "-d, --delay <delay_in_ms>",
"delay in milliseconds(ms) between consecutive requests within a collection" "delay in milliseconds(ms) between consecutive requests within a collection"

View File

@@ -33,4 +33,5 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalHeaders: { key: string; value: string; active: boolean }[]; effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
effectiveFinalParams: { key: string; value: string; active: boolean }[]; effectiveFinalParams: { key: string; value: string; active: boolean }[];
effectiveFinalBody: FormData | string | null; effectiveFinalBody: FormData | string | null;
effectiveFinalMaskedURL: string;
} }

View File

@@ -1,45 +1,50 @@
import { error } from "../../types/errors"; import { error } from "../../types/errors";
import { import { HoppEnvs, HoppEnvPair } from "../../types/request";
HoppEnvs,
HoppEnvPair,
HoppEnvKeyPairObject,
HoppEnvExportObject,
HoppBulkEnvExportObject,
} from "../../types/request";
import { readJsonFile } from "../../utils/mutators"; import { readJsonFile } from "../../utils/mutators";
/** /**
* Parses env json file for given path and validates the parsed env json object. * Parses env json file for given path and validates the parsed env json object.
* @param path Path of env.json file to be parsed. * @param path Path of env.json file to be parsed.
* @param envName Name of the environment that should be used. If undefined first environment is used.
* @returns For successful parsing we get HoppEnvs object. * @returns For successful parsing we get HoppEnvs object.
*/ */
export async function parseEnvsData(path: string) { export async function parseEnvsData(path: string, envName: string | undefined) {
const contents = await readJsonFile(path); const contents = await readJsonFile(path)
const envPairs: Array<HoppEnvPair> = []; if(!(contents && typeof contents === "object" && Array.isArray(contents))) {
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents); throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
const HoppBulkEnvExportObjectResult =
HoppBulkEnvExportObject.safeParse(contents);
// CLI doesnt support bulk environments export.
// Hence we check for this case and throw an error if it matches the format.
if (HoppBulkEnvExportObjectResult.success) {
throw error({ code: "BULK_ENV_FILE", path, data: error });
} }
// Checks if the environment file is of the correct format. const envPairs: Array<HoppEnvPair> = []
// 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 });
}
if (HoppEnvKeyPairResult.success) { const contentEntries = Object.entries(contents)
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) { let environmentFound = false;
envPairs.push({ key, value });
for(const [key, obj] of contentEntries) {
if(!(typeof obj === "object" && "name" in obj && "variables" in obj)) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: { value: obj } })
} }
} else if (HoppEnvExportObjectResult.success) { if(envName && envName !== obj.name) {
const { key, value } = HoppEnvExportObjectResult.data.variables[0]; continue
envPairs.push({ key, value }); }
environmentFound = true;
for(const variable of obj.variables) {
if(
!(
typeof variable === "object" &&
"key" in variable &&
"value" in variable &&
"secret" in variable
)
) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: { value: variable } });
}
const { key, value, secret } = variable;
envPairs.push({ key: key, value: value, secret: secret });
}
break
} }
if(envName && !environmentFound) {
return <HoppEnvs>{ global: [], selected: envPairs }; throw error({ code: "ENVIRONMENT_NAME_NOT_FOUND", data: envName });
}
return <HoppEnvs>{ global: [], selected: envPairs }
} }

View File

@@ -1,6 +1,7 @@
export type TestCmdOptions = { export type TestCmdOptions = {
env: string | undefined; env: string | undefined;
delay: string | undefined; delay: string | undefined;
envName: string | undefined;
}; };
export type HOPP_ENV_FILE_EXT = "json"; export type HOPP_ENV_FILE_EXT = "json";

View File

@@ -15,6 +15,7 @@ type HoppErrors = {
FILE_NOT_FOUND: HoppErrorPath; FILE_NOT_FOUND: HoppErrorPath;
UNKNOWN_COMMAND: HoppErrorCmd; UNKNOWN_COMMAND: HoppErrorCmd;
MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData; MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData;
ENVIRONMENT_NAME_NOT_FOUND: HoppErrorData;
NO_FILE_PATH: {}; NO_FILE_PATH: {};
PRE_REQUEST_SCRIPT_ERROR: HoppErrorData; PRE_REQUEST_SCRIPT_ERROR: HoppErrorData;
PARSING_ERROR: HoppErrorData; PARSING_ERROR: HoppErrorData;
@@ -24,7 +25,6 @@ type HoppErrors = {
REQUEST_ERROR: HoppErrorData; REQUEST_ERROR: HoppErrorData;
INVALID_ARGUMENT: HoppErrorData; INVALID_ARGUMENT: HoppErrorData;
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData; MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
INVALID_FILE_TYPE: HoppErrorData; INVALID_FILE_TYPE: HoppErrorData;
}; };

View File

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

View File

@@ -1,7 +1,7 @@
import { bold } from "chalk"; import { bold } from "chalk";
import { groupEnd, group, log } from "console"; import { groupEnd, group, log } from "console";
import { handleError } from "../handlers/error"; import { handleError } from "../handlers/error";
import { RequestConfig } from "../interfaces/request"; import { Method } from "axios";
import { RequestRunnerResponse, TestReport } from "../interfaces/response"; import { RequestRunnerResponse, TestReport } from "../interfaces/response";
import { HoppCLIError } from "../types/errors"; import { HoppCLIError } from "../types/errors";
import { import {
@@ -172,11 +172,12 @@ export const printFailedTestsReport = (
export const printRequestRunner = { export const printRequestRunner = {
/** /**
* Request-runner starting message. * Request-runner starting message.
* @param requestConfig Provides request's method and url. * @param requestMethod Provides request's method
* @param maskedURL Provides the URL with secrets masked with asterisks
*/ */
start: (requestConfig: RequestConfig) => { start: (requestMethod: Method | undefined, maskedURL: string) => {
const METHOD = BG_INFO(` ${requestConfig.method} `); const METHOD = BG_INFO(` ${requestMethod} `);
const ENDPOINT = requestConfig.url; const ENDPOINT = maskedURL;
process.stdout.write(`${METHOD} ${ENDPOINT}`); process.stdout.write(`${METHOD} ${ENDPOINT}`);
}, },

View File

@@ -50,9 +50,9 @@ export const preRequestScriptRunner = (
isHoppCLIError(reason) isHoppCLIError(reason)
? reason ? reason
: error({ : error({
code: "PRE_REQUEST_SCRIPT_ERROR", code: "PRE_REQUEST_SCRIPT_ERROR",
data: reason, data: reason,
}) })
) )
); );
@@ -151,6 +151,12 @@ export function getEffectiveRESTRequest(
request.endpoint, request.endpoint,
envVariables envVariables
); );
const maskedEnvVariables = setAllEnvironmentValuesToAsterisk(envVariables)
const _effectiveFinalMaskedURL = parseTemplateStringE(
request.endpoint,
maskedEnvVariables)
if (E.isLeft(_effectiveFinalURL)) { if (E.isLeft(_effectiveFinalURL)) {
return E.left( return E.left(
error({ error({
@@ -160,6 +166,7 @@ export function getEffectiveRESTRequest(
); );
} }
const effectiveFinalURL = _effectiveFinalURL.right; const effectiveFinalURL = _effectiveFinalURL.right;
const effectiveFinalMaskedURL = E.isLeft(_effectiveFinalMaskedURL) ? request.endpoint : _effectiveFinalMaskedURL.right;
return E.right({ return E.right({
...request, ...request,
@@ -167,6 +174,7 @@ export function getEffectiveRESTRequest(
effectiveFinalHeaders, effectiveFinalHeaders,
effectiveFinalParams, effectiveFinalParams,
effectiveFinalBody, effectiveFinalBody,
effectiveFinalMaskedURL,
}); });
} }
@@ -242,15 +250,15 @@ function getFinalBodyFromRequest(
arrayFlatMap((x) => arrayFlatMap((x) =>
x.isFile x.isFile
? x.value.map((v) => ({ ? x.value.map((v) => ({
key: parseTemplateString(x.key, envVariables), key: parseTemplateString(x.key, envVariables),
value: v as string | Blob, value: v as string | Blob,
})) }))
: [ : [
{ {
key: parseTemplateString(x.key, envVariables), key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables), value: parseTemplateString(x.value, envVariables),
}, },
] ]
), ),
toFormData, toFormData,
E.right E.right
@@ -287,3 +295,22 @@ export const getPreRequestMetrics = (
hasPreReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 }, hasPreReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
(scripts) => <PreRequestMetrics>{ scripts, duration } (scripts) => <PreRequestMetrics>{ scripts, duration }
); );
/**
* Mask all environment values with asterisks
* @param variables Environment variable array
* @returns Environment variable array with masked values
*/
const setAllEnvironmentValuesToAsterisk = (
variables: Environment["variables"]
): Environment["variables"] => {
const envVariables: Environment["variables"] = [];
for (const variable of variables) {
let value = variable.value
if (variable.secret) {
value = "******"
}
envVariables.push({ key: variable.key, secret: variable.secret, value: value })
}
return envVariables
}

View File

@@ -220,6 +220,7 @@ export const processRequest =
effectiveFinalHeaders: [], effectiveFinalHeaders: [],
effectiveFinalParams: [], effectiveFinalParams: [],
effectiveFinalURL: "", effectiveFinalURL: "",
effectiveFinalMaskedURL:"",
}; };
// Executing pre-request-script // Executing pre-request-script
@@ -237,8 +238,8 @@ export const processRequest =
// Creating request-config for request-runner. // Creating request-config for request-runner.
const requestConfig = createRequest(effectiveRequest); const requestConfig = createRequest(effectiveRequest);
printRequestRunner.start(requestConfig); printRequestRunner.start(requestConfig.method, effectiveRequest.effectiveFinalMaskedURL);
// Default value for request-runner's response. // Default value for request-runner's response.
let _requestRunnerRes: RequestRunnerResponse = { let _requestRunnerRes: RequestRunnerResponse = {

View File

@@ -137,10 +137,10 @@ a {
.cm-tooltip { .cm-tooltip {
.tippy-box { .tippy-box {
@apply shadow-none #{!important}; @apply shadow-none;
@apply fixed; @apply fixed;
@apply inline-flex; @apply inline-flex;
@apply -mt-7.5; @apply -mt-8;
} }
} }

View File

@@ -48,7 +48,8 @@
"turn_off": "Ausschalten", "turn_off": "Ausschalten",
"turn_on": "Einschalten", "turn_on": "Einschalten",
"undo": "Rückgängig machen", "undo": "Rückgängig machen",
"yes": "Ja" "yes": "Ja",
"secret": "Als Secret speichern"
}, },
"add": { "add": {
"new": "Neue hinzufügen", "new": "Neue hinzufügen",
@@ -880,4 +881,4 @@
"team": "Team Workspace", "team": "Team Workspace",
"title": "Workspaces" "title": "Workspaces"
} }
} }

View File

@@ -1,6 +1,5 @@
{ {
"action": { "action": {
"add": "Add",
"autoscroll": "Autoscroll", "autoscroll": "Autoscroll",
"cancel": "Cancel", "cancel": "Cancel",
"choose_file": "Choose a file", "choose_file": "Choose a file",
@@ -49,34 +48,16 @@
"turn_off": "Turn off", "turn_off": "Turn off",
"turn_on": "Turn on", "turn_on": "Turn on",
"undo": "Undo", "undo": "Undo",
"yes": "Yes" "yes": "Yes",
"secret": "Save as secret"
}, },
"add": { "add": {
"new": "Add new", "new": "Add new",
"star": "Add star" "star": "Add star"
}, },
"cookies": {
"modal": {
"new_domain_name": "New domain name",
"set": "Set a cookie",
"cookie_string": "Cookie string",
"enter_cookie_string": "Enter cookie string",
"cookie_name": "Name",
"cookie_value": "Value",
"cookie_path": "Path",
"cookie_expires": "Expires",
"managed_tab": "Managed",
"raw_tab": "Raw",
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
"empty_domains": "Domain list is empty",
"empty_domain": "Domain is empty",
"no_cookies_in_domain": "No cookies set for this domain"
}
},
"app": { "app": {
"chat_with_us": "Chat with us", "chat_with_us": "Chat with us",
"contact_us": "Contact us", "contact_us": "Contact us",
"cookies": "Cookies",
"copy": "Copy", "copy": "Copy",
"copy_user_id": "Copy User Auth Token", "copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options", "developer_option": "Developer options",
@@ -132,7 +113,6 @@
}, },
"authorization": { "authorization": {
"generate_token": "Generate Token", "generate_token": "Generate Token",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Include in URL", "include_in_url": "Include in URL",
"learn": "Learn how", "learn": "Learn how",
"pass_key_by": "Pass by", "pass_key_by": "Pass by",
@@ -145,7 +125,6 @@
"created": "Collection created", "created": "Collection created",
"different_parent": "Cannot reorder collection with different parent", "different_parent": "Cannot reorder collection with different parent",
"edit": "Edit Collection", "edit": "Edit Collection",
"import_or_create": "Import or create a collection",
"invalid_name": "Please provide a name for the collection", "invalid_name": "Please provide a name for the collection",
"invalid_root_move": "Collection already in the root", "invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully", "moved": "Moved Successfully",
@@ -231,7 +210,6 @@
"empty_variables": "No variables", "empty_variables": "No variables",
"global": "Global", "global": "Global",
"global_variables": "Global variables", "global_variables": "Global variables",
"import_or_create": "Import or create a environment",
"invalid_name": "Please provide a name for the environment", "invalid_name": "Please provide a name for the environment",
"list": "Environment variables", "list": "Environment variables",
"my_environments": "My Environments", "my_environments": "My Environments",
@@ -272,9 +250,7 @@
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again", "json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
"network_error": "There seems to be a network error. Please try again.", "network_error": "There seems to be a network error. Please try again.",
"network_fail": "Could not send request", "network_fail": "Could not send request",
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
"no_duration": "No duration", "no_duration": "No duration",
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
"no_results_found": "No matches found", "no_results_found": "No matches found",
"page_not_found": "This page could not be found", "page_not_found": "This page could not be found",
"proxy_error": "Proxy error", "proxy_error": "Proxy error",
@@ -481,7 +457,6 @@
"enter_curl": "Enter cURL command", "enter_curl": "Enter cURL command",
"generate_code": "Generate code", "generate_code": "Generate code",
"generated_code": "Generated code", "generated_code": "Generated code",
"go_to_authorization_tab": "Go to Authorization",
"header_list": "Header List", "header_list": "Header List",
"invalid_name": "Please provide a name for the request", "invalid_name": "Please provide a name for the request",
"method": "Method", "method": "Method",
@@ -769,11 +744,9 @@
"disconnected_from": "Disconnected from {name}", "disconnected_from": "Disconnected from {name}",
"docs_generated": "Documentation generated", "docs_generated": "Documentation generated",
"download_started": "Download started", "download_started": "Download started",
"download_failed": "Download failed",
"enabled": "Enabled", "enabled": "Enabled",
"file_imported": "File imported", "file_imported": "File imported",
"finished_in": "Finished in {duration} ms", "finished_in": "Finished in {duration} ms",
"hide": "Hide",
"history_deleted": "History deleted", "history_deleted": "History deleted",
"linewrap": "Wrap lines", "linewrap": "Wrap lines",
"loading": "Loading...", "loading": "Loading...",
@@ -784,7 +757,6 @@
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}", "published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
"published_message": "Published message: {message} to topic: {topic}", "published_message": "Published message: {message} to topic: {topic}",
"reconnection_error": "Failed to reconnect", "reconnection_error": "Failed to reconnect",
"show": "Show",
"subscribed_failed": "Failed to subscribe to topic: {topic}", "subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}", "subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}", "unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
@@ -866,7 +838,7 @@
"new": "New Team", "new": "New Team",
"new_created": "New team created", "new_created": "New team created",
"new_name": "My New Team", "new_name": "My New Team",
"no_access": "You do not have edit access to this team", "no_access": "You do not have edit access to these collections",
"no_invite_found": "Invitation not found. Contact your team owner.", "no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.", "no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.", "not_found": "Team not found. Contact your team owner.",

View File

@@ -5,7 +5,7 @@
"choose_file": "選擇一個檔案", "choose_file": "選擇一個檔案",
"clear": "清除", "clear": "清除",
"clear_all": "全部清除", "clear_all": "全部清除",
"clear_history": "清除所有歷史記錄", "clear_history": "Clear all History",
"close": "關閉", "close": "關閉",
"connect": "連線", "connect": "連線",
"connecting": "正在連接", "connecting": "正在連接",
@@ -79,8 +79,8 @@
"search": "搜尋", "search": "搜尋",
"share": "分享", "share": "分享",
"shortcuts": "快捷方式", "shortcuts": "快捷方式",
"social_description": "在社交媒體上追蹤我們即可在第一時間得知新聞、更新、以及新版本的消息。", "social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
"social_links": "社群連結", "social_links": "Social links",
"spotlight": "聚光燈", "spotlight": "聚光燈",
"status": "狀態", "status": "狀態",
"status_description": "檢查網站狀態", "status_description": "檢查網站狀態",
@@ -135,15 +135,15 @@
"renamed": "集合已重新命名", "renamed": "集合已重新命名",
"request_in_use": "請求正在使用中", "request_in_use": "請求正在使用中",
"save_as": "另存為", "save_as": "另存為",
"save_to_collection": "儲存到集合", "save_to_collection": "Save to Collection",
"select": "選擇一個集合", "select": "選擇一個集合",
"select_location": "選擇位置", "select_location": "選擇位置",
"select_team": "選擇一個團隊", "select_team": "選擇一個團隊",
"team_collections": "團隊集合" "team_collections": "團隊集合"
}, },
"confirm": { "confirm": {
"close_unsaved_tab": "您確定要關閉此分頁嗎?", "close_unsaved_tab": "Are you sure you want to close this tab?",
"close_unsaved_tabs": "您確定要關閉所有分頁嗎?{count} 個未儲存的分頁將會遺失。", "close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
"exit_team": "您確定要離開此團隊嗎?", "exit_team": "您確定要離開此團隊嗎?",
"logout": "您確定要登出嗎?", "logout": "您確定要登出嗎?",
"remove_collection": "您確定要永久刪除該集合嗎?", "remove_collection": "您確定要永久刪除該集合嗎?",
@@ -158,9 +158,9 @@
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。" "sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
}, },
"context_menu": { "context_menu": {
"add_parameters": "新增至參數", "add_parameters": "Add to parameters",
"open_request_in_new_tab": "在新分頁開啟請求", "open_request_in_new_tab": "Open request in new tab",
"set_environment_variable": "設為變數" "set_environment_variable": "Set as variable"
}, },
"count": { "count": {
"header": "請求標頭 {count}", "header": "請求標頭 {count}",
@@ -204,31 +204,31 @@
"create_new": "建立新環境", "create_new": "建立新環境",
"created": "已建立環境", "created": "已建立環境",
"deleted": "刪除環境", "deleted": "刪除環境",
"duplicated": "已複製環境", "duplicated": "Environment duplicated",
"edit": "編輯環境", "edit": "編輯環境",
"empty_variables": "無變數", "empty_variables": "No variables",
"global": "全域", "global": "Global",
"global_variables": "全域變數", "global_variables": "Global variables",
"invalid_name": "請提供有效的環境名稱", "invalid_name": "請提供有效的環境名稱",
"list": "環境變數", "list": "Environment variables",
"my_environments": "我的環境", "my_environments": "我的環境",
"name": "名稱", "name": "Name",
"nested_overflow": "巢狀環境變數不得大於 10 層", "nested_overflow": "巢狀環境變數不得大於 10 層",
"new": "建立環境", "new": "建立環境",
"no_active_environment": "無使用中的環境", "no_active_environment": "No active environment",
"no_environment": "無環境", "no_environment": "無環境",
"no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。", "no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。",
"quick_peek": "快速預覽環境", "quick_peek": "Environment Quick Peek",
"replace_with_variable": "以變數替代", "replace_with_variable": "Replace with variable",
"scope": "範圍", "scope": "Scope",
"select": "選擇環境", "select": "選擇環境",
"set": "設定環境", "set": "Set environment",
"set_as_environment": "設為環境", "set_as_environment": "Set as environment",
"team_environments": "團隊環境", "team_environments": "團隊環境",
"title": "環境", "title": "環境",
"updated": "更新環境", "updated": "更新環境",
"value": "數值", "value": "Value",
"variable": "變數", "variable": "Variable",
"variable_list": "變數列表" "variable_list": "變數列表"
}, },
"error": { "error": {
@@ -252,7 +252,7 @@
"no_duration": "無持續時間", "no_duration": "無持續時間",
"no_results_found": "找不到結果", "no_results_found": "找不到結果",
"page_not_found": "找不到此頁面", "page_not_found": "找不到此頁面",
"proxy_error": "Proxy 錯誤", "proxy_error": "Proxy error",
"script_fail": "無法執行預請求指令碼", "script_fail": "無法執行預請求指令碼",
"something_went_wrong": "發生了一些錯誤", "something_went_wrong": "發生了一些錯誤",
"test_script_fail": "無法執行測試指令碼" "test_script_fail": "無法執行測試指令碼"
@@ -278,13 +278,13 @@
"renamed": "資料夾已重新命名" "renamed": "資料夾已重新命名"
}, },
"graphql": { "graphql": {
"connection_switch_confirm": "您要使用最新的 GraphQL 端點連線嗎?", "connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
"connection_switch_new_url": "切換至分頁將斷開使用中的 GraphQL 連線。新的連線網址為 ", "connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
"connection_switch_url": "您已連接至 GraphQL 端點。連線網址為 ", "connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
"mutations": "變體", "mutations": "變體",
"schema": "綱要", "schema": "綱要",
"subscriptions": "訂閱", "subscriptions": "訂閱",
"switch_connection": "切換連線" "switch_connection": "Switch connection"
}, },
"group": { "group": {
"time": "時間", "time": "時間",
@@ -339,27 +339,27 @@
"title": "匯入" "title": "匯入"
}, },
"inspections": { "inspections": {
"description": "檢查潛在錯誤", "description": "Inspect possible errors",
"environment": { "environment": {
"add_environment": "新增至環境", "add_environment": "Add to Environment",
"not_found": "找不到環境變數 “{environment}”" "not_found": "Environment variable “{environment}” not found."
}, },
"header": { "header": {
"cookie": "瀏覽器不允許 Hoppscotch 設定 Cookie 標頭。在我們推出 Hoppscotch 桌面版前,請先使用 Authorization 標頭。" "cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
}, },
"response": { "response": {
"401_error": "請檢查您的授權認證。", "401_error": "Please check your authentication credentials.",
"404_error": "請檢查您的請求網址和方式類型。", "404_error": "Please check your request URL and method type.",
"cors_error": "請檢查您的跨來源資源共用設定。", "cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
"default_error": "請檢查您的請求。", "default_error": "Please check your request.",
"network_error": "請檢查您的網路連線。" "network_error": "Please check your network connection."
}, },
"title": "檢查工具", "title": "Inspector",
"url": { "url": {
"extension_not_installed": "未安裝擴充套件。", "extension_not_installed": "Extension not installed.",
"extension_unknown_origin": "請確認您是否已將 API 端點的來源加入 Hoppscotch 擴充套件的清單。", "extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
"extention_enable_action": "啟用瀏覽器擴充套件", "extention_enable_action": "Enable Browser Extension",
"extention_not_enabled": "未啟用擴充套件。" "extention_not_enabled": "Extension not enabled."
} }
}, },
"layout": { "layout": {
@@ -472,7 +472,7 @@
"payload": "負載", "payload": "負載",
"query": "查詢", "query": "查詢",
"raw_body": "原始請求本體", "raw_body": "原始請求本體",
"rename": "重新命名請求", "rename": "Rename Request",
"renamed": "請求已重新命名", "renamed": "請求已重新命名",
"run": "執行", "run": "執行",
"save": "儲存", "save": "儲存",
@@ -510,7 +510,7 @@
"accent_color": "強調色", "accent_color": "強調色",
"account": "帳號", "account": "帳號",
"account_deleted": "已刪除您的帳號", "account_deleted": "已刪除您的帳號",
"account_description": "自您的帳號設定。", "account_description": "自定義您的帳號設定。",
"account_email_description": "您的主要電子郵件地址。", "account_email_description": "您的主要電子郵件地址。",
"account_name_description": "這是您的顯示名稱。", "account_name_description": "這是您的顯示名稱。",
"background": "背景", "background": "背景",
@@ -542,7 +542,7 @@
"read_the": "閱讀", "read_the": "閱讀",
"reset_default": "重置為預設", "reset_default": "重置為預設",
"short_codes": "快捷碼", "short_codes": "快捷碼",
"short_codes_description": "您建立的快捷碼。", "short_codes_description": "我們為您打造的快捷碼。",
"sidebar_on_left": "左側邊欄", "sidebar_on_left": "左側邊欄",
"sync": "同步", "sync": "同步",
"sync_collections": "集合", "sync_collections": "集合",
@@ -551,9 +551,9 @@
"sync_history": "歷史", "sync_history": "歷史",
"system_mode": "系統", "system_mode": "系統",
"telemetry": "遙測服務", "telemetry": "遙測服務",
"telemetry_helps_us": "遙測服務能夠幫助我們進行個化操作,為您提供最佳體驗。", "telemetry_helps_us": "遙測服務幫助我們進行個化操作,為您提供最佳體驗。",
"theme": "主題", "theme": "主題",
"theme_description": "自您的應用程式主題。", "theme_description": "自定義您的應用程式主題。",
"use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄", "use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄",
"user": "使用者", "user": "使用者",
"verified_email": "已確認電子郵件地址", "verified_email": "已確認電子郵件地址",
@@ -592,26 +592,26 @@
"title": "導航" "title": "導航"
}, },
"others": { "others": {
"prettify": "美化編輯器的內容", "prettify": "Prettify Editor's Content",
"title": "其他" "title": "Others"
}, },
"request": { "request": {
"copy_request_link": "複製請求連結", "copy_request_link": "複製請求連結",
"delete_method": "選擇 DELETE 方法", "delete_method": "選擇 DELETE 方法",
"get_method": "選擇 GET 方法", "get_method": "選擇 GET 方法",
"head_method": "選擇 HEAD 方法", "head_method": "選擇 HEAD 方法",
"import_curl": "匯入 cURL", "import_curl": "Import cURL",
"method": "方法", "method": "方法",
"next_method": "選擇下一個方法", "next_method": "選擇下一個方法",
"post_method": "選擇 POST 方法", "post_method": "選擇 POST 方法",
"previous_method": "選擇上一個方法", "previous_method": "選擇上一個方法",
"put_method": "選擇 PUT 方法", "put_method": "選擇 PUT 方法",
"rename": "重新命名請求", "rename": "Rename Request",
"reset_request": "重置請求", "reset_request": "重置請求",
"save_request": "儲存請求", "save_request": "Save Request",
"save_to_collections": "儲存到集合", "save_to_collections": "儲存到集合",
"send_request": "傳送請求", "send_request": "傳送請求",
"show_code": "產生程式碼片段", "show_code": "Generate code snippet",
"title": "請求" "title": "請求"
}, },
"response": { "response": {
@@ -642,82 +642,82 @@
"url": "網址" "url": "網址"
}, },
"spotlight": { "spotlight": {
"change_language": "變更語言", "change_language": "Change Language",
"environments": { "environments": {
"delete": "刪除目前環境", "delete": "Delete current environment",
"duplicate": "複製目前環境", "duplicate": "Duplicate current environment",
"duplicate_global": "複製全域環境", "duplicate_global": "Duplicate global environment",
"edit": "編輯目前環境", "edit": "Edit current environment",
"edit_global": "編輯全域環境", "edit_global": "Edit global environment",
"new": "建立新環境", "new": "Create new environment",
"new_variable": "建立新環境變數", "new_variable": "Create a new environment variable",
"title": "環境" "title": "Environments"
}, },
"general": { "general": {
"chat": "與客服對話", "chat": "Chat with support",
"help_menu": "幫助與支援", "help_menu": "Help and support",
"open_docs": "閱讀說明文件", "open_docs": "Read Documentation",
"open_github": "開啟 GitHub 儲存庫", "open_github": "Open GitHub repository",
"open_keybindings": "鍵盤快捷鍵", "open_keybindings": "Keyboard shortcuts",
"social": "社交", "social": "Social",
"title": "一般" "title": "General"
}, },
"graphql": { "graphql": {
"connect": "連接至伺服器", "connect": "Connect to server",
"disconnect": "斷開與伺服器的連線" "disconnect": "Disconnect from server"
}, },
"miscellaneous": { "miscellaneous": {
"invite": "邀請您的朋友使用 Hoppscotch", "invite": "Invite your friends to Hoppscotch",
"title": "雜項" "title": "Miscellaneous"
}, },
"request": { "request": {
"save_as_new": "儲存為新請求", "save_as_new": "Save as new request",
"select_method": "選擇方法", "select_method": "Select method",
"switch_to": "切換至", "switch_to": "Switch to",
"tab_authorization": "授權分頁", "tab_authorization": "Authorization tab",
"tab_body": "本體分頁", "tab_body": "Body tab",
"tab_headers": "標頭分頁", "tab_headers": "Headers tab",
"tab_parameters": "參數分頁", "tab_parameters": "Parameters tab",
"tab_pre_request_script": "預請求腳本分頁", "tab_pre_request_script": "Pre-request script tab",
"tab_query": "查詢分頁", "tab_query": "Query tab",
"tab_tests": "測試分頁", "tab_tests": "Tests tab",
"tab_variables": "變數分頁" "tab_variables": "Variables tab"
}, },
"response": { "response": {
"copy": "複製回應", "copy": "Copy response",
"download": "下載回應", "download": "Download response as file",
"title": "回應" "title": "Response"
}, },
"section": { "section": {
"interceptor": "攔截器", "interceptor": "Interceptor",
"interface": "介面", "interface": "Interface",
"theme": "主題", "theme": "Theme",
"user": "使用者" "user": "User"
}, },
"settings": { "settings": {
"change_interceptor": "變更攔截器", "change_interceptor": "Change Interceptor",
"change_language": "變更語言", "change_language": "Change Language",
"theme": { "theme": {
"black": "黑色", "black": "Black",
"dark": "暗色", "dark": "Dark",
"light": "亮色", "light": "Light",
"system": "跟隨系統" "system": "System preference"
} }
}, },
"tab": { "tab": {
"close_current": "關閉目前分頁", "close_current": "Close current tab",
"close_others": "關閉所有其他分頁", "close_others": "Close all other tabs",
"duplicate": "複製目前分頁", "duplicate": "Duplicate current tab",
"new_tab": "開啟新分頁", "new_tab": "Open a new tab",
"title": "分頁" "title": "Tabs"
}, },
"workspace": { "workspace": {
"delete": "刪除目前團隊", "delete": "Delete current team",
"edit": "編輯目前團隊", "edit": "Edit current team",
"invite": "邀請他人加入團隊", "invite": "Invite people to team",
"new": "建立新團隊", "new": "Create new team",
"switch_to_personal": "切換至您的個人工作區", "switch_to_personal": "Switch to your personal workspace",
"title": "團隊" "title": "Teams"
} }
}, },
"sse": { "sse": {
@@ -777,11 +777,11 @@
"tab": { "tab": {
"authorization": "授權", "authorization": "授權",
"body": "請求本體", "body": "請求本體",
"close": "關閉分頁", "close": "Close Tab",
"close_others": "關閉其他分頁", "close_others": "Close other Tabs",
"collections": "集合", "collections": "集合",
"documentation": "幫助文件", "documentation": "幫助文件",
"duplicate": "複製分頁", "duplicate": "Duplicate Tab",
"environments": "環境", "environments": "環境",
"headers": "請求標頭", "headers": "請求標頭",
"history": "歷史記錄", "history": "歷史記錄",

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 386 B

View File

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

Before

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

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

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,11 +1,11 @@
// generated by unplugin-vue-components /* eslint-disable */
// We suggest you to commit this file into source control /* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {} export {}
declare module '@vue/runtime-core' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default'] AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default'] AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
@@ -58,8 +58,6 @@ declare module '@vue/runtime-core' {
CollectionsRequest: typeof import('./components/collections/Request.vue')['default'] CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default'] CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default'] Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default'] EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default'] EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
@@ -92,6 +90,7 @@ declare module '@vue/runtime-core' {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'] HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'] HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'] HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox'] HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand'] HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
@@ -186,6 +185,7 @@ declare module '@vue/runtime-core' {
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default'] SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default'] SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default'] SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default'] SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default'] SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default'] SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
@@ -218,5 +218,4 @@ declare module '@vue/runtime-core' {
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default'] WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default'] WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
} }
} }

View File

@@ -2,16 +2,53 @@
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" /> <AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
<AppShare :show="showShare" @hide-modal="showShare = false" /> <AppShare :show="showShare" @hide-modal="showShare = false" />
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" /> <FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@hide-modal="confirmRemove = false"
@resolve="deleteTeam()"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import { defineActionHandler } from "~/helpers/actions" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { useToast } from "~/composables/toast"
import { useI18n } from "~/composables/i18n"
const toast = useToast()
const t = useI18n()
const showShortcuts = ref(false) const showShortcuts = ref(false)
const showShare = ref(false) const showShare = ref(false)
const showLogin = ref(false) const showLogin = ref(false)
const confirmRemove = ref(false)
const teamID = ref<string | null>(null)
const deleteTeam = () => {
if (!teamID.value) return
pipe(
backendDeleteTeam(teamID.value),
TE.match(
(err) => {
// TODO: Better errors ? We know the possible errors now
toast.error(`${t("error.something_went_wrong")}`)
console.error(err)
},
() => {
invokeAction("workspace.switch.personal")
toast.success(`${t("team.deleted")}`)
}
)
)() // Tasks (and TEs) are lazy, so call the function returned
}
defineActionHandler("flyouts.keybinds.toggle", () => { defineActionHandler("flyouts.keybinds.toggle", () => {
showShortcuts.value = !showShortcuts.value showShortcuts.value = !showShortcuts.value
}) })
@@ -23,4 +60,9 @@ defineActionHandler("modals.share.toggle", () => {
defineActionHandler("modals.login.toggle", () => { defineActionHandler("modals.login.toggle", () => {
showLogin.value = !showLogin.value showLogin.value = !showLogin.value
}) })
defineActionHandler("modals.team.delete", ({ teamId }) => {
teamID.value = teamId
confirmRemove.value = true
})
</script> </script>

View File

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

View File

@@ -1,9 +1,7 @@
<template> <template>
<div> <div>
<header <header
ref="headerRef"
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden" class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
> >
<div <div
class="inline-flex items-center justify-start flex-1 space-x-2" class="inline-flex items-center justify-start flex-1 space-x-2"
@@ -233,39 +231,29 @@
@invite-team="inviteTeam(editingTeamName, editingTeamID)" @invite-team="inviteTeam(editingTeamName, editingTeamID)"
@refetch-teams="refetchTeams" @refetch-teams="refetchTeams"
/> />
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@hide-modal="confirmRemove = false"
@resolve="deleteTeam"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref, watch } from "vue"
import IconUser from "~icons/lucide/user"
import IconUsers from "~icons/lucide/users"
import IconSettings from "~icons/lucide/settings"
import IconDownload from "~icons/lucide/download"
import IconUploadCloud from "~icons/lucide/upload-cloud"
import IconUserPlus from "~icons/lucide/user-plus"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
import { platform } from "~/platform"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { defineActionHandler, invokeAction } from "@helpers/actions" import { defineActionHandler, invokeAction } from "@helpers/actions"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { useToast } from "~/composables/toast"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { installPWA, pwaDefferedPrompt } from "@modules/pwa"
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
import { computed, reactive, ref, watch } from "vue"
import { useToast } from "~/composables/toast"
import { GetMyTeamsQuery, TeamMemberRole } from "~/helpers/backend/graphql"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { platform } from "~/platform"
import IconDownload from "~icons/lucide/download"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import IconSettings from "~icons/lucide/settings"
import IconUploadCloud from "~icons/lucide/upload-cloud"
import IconUser from "~icons/lucide/user"
import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -290,9 +278,6 @@ const currentUser = useReadonlyStream(
platform.auth.getProbableUser() platform.auth.getProbableUser()
) )
const confirmRemove = ref(false)
const teamID = ref<string | null>(null)
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>() const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
// TeamList-Adapter // TeamList-Adapter
@@ -392,24 +377,6 @@ const handleTeamEdit = () => {
} }
} }
const deleteTeam = () => {
if (!teamID.value) return
pipe(
backendDeleteTeam(teamID.value),
TE.match(
(err) => {
// TODO: Better errors ? We know the possible errors now
toast.error(`${t("error.something_went_wrong")}`)
console.error(err)
},
() => {
invokeAction("workspace.switch.personal")
toast.success(`${t("team.deleted")}`)
}
)
)() // Tasks (and TEs) are lazy, so call the function returned
}
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
const profile = ref<any | null>(null) const profile = ref<any | null>(null)
@@ -438,12 +405,6 @@ defineActionHandler(
computed(() => !currentUser.value) computed(() => !currentUser.value)
) )
defineActionHandler("modals.team.delete", ({ teamId }) => {
if (selectedTeam.value?.myRole !== TeamMemberRole.Owner) return noPermission()
teamID.value = teamId
confirmRemove.value = true
})
const noPermission = () => { const noPermission = () => {
toast.error(`${t("profile.no_permission")}`) toast.error(`${t("profile.no_permission")}`)
} }

View File

@@ -37,8 +37,7 @@
import { ref, watch } from "vue" import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useService } from "dioc/vue" import { currentActiveTab } from "~/helpers/rest/tab"
import { RESTTabService } from "~/services/tab/rest"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
@@ -61,12 +60,11 @@ const emit = defineEmits<{
const editingName = ref("") const editingName = ref("")
const tabs = useService(RESTTabService)
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
editingName.value = tabs.currentActiveTab.value.document.request.name editingName.value = currentActiveTab.value.document.request.name
} }
} }
) )

View File

@@ -25,7 +25,7 @@
<HoppButtonSecondary <HoppButtonSecondary
v-if="!saveRequest" v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconImport" :icon="IconArchive"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@click="emit('display-modal-import-export')" @click="emit('display-modal-import-export')"
/> />
@@ -257,27 +257,12 @@
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
> >
<div class="flex flex-col items-center space-y-4"> <HoppButtonSecondary
<span class="text-secondaryLight text-center"> :label="t('add.new')"
{{ t("collection.import_or_create") }} filled
</span> outline
<div class="flex gap-4 flex-col items-stretch"> @click="emit('display-modal-add')"
<HoppButtonPrimary />
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="emit('display-modal-import-export')"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'" v-else-if="node.data.type === 'collections'"
@@ -303,7 +288,8 @@
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`" :alt="`${t('empty.folder')}`"
:text="t('empty.folder')" :text="t('empty.folder')"
/> >
</HoppSmartPlaceholder>
</template> </template>
</HoppSmartTree> </HoppSmartTree>
</div> </div>
@@ -311,9 +297,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconArchive from "~icons/lucide/archive"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed, PropType, Ref, toRef } from "vue" import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
@@ -326,8 +312,7 @@ import { useColorMode } from "@composables/theming"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js" import { Picked } from "~/helpers/types/HoppPicked.js"
import { useService } from "dioc/vue" import { currentActiveTab } from "~/helpers/rest/tab"
import { RESTTabService } from "~/services/tab/rest"
export type Collection = { export type Collection = {
type: "collections" type: "collections"
@@ -535,8 +520,7 @@ const isSelected = ({
} }
} }
const tabs = useService(RESTTabService) const active = computed(() => currentActiveTab.value.document.saveContext)
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
const isActiveRequest = (folderPath: string, requestIndex: number) => { const isActiveRequest = (folderPath: string, requestIndex: number) => {
return pipe( return pipe(

View File

@@ -82,16 +82,12 @@ import {
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core" import { computedWithControl } from "@vueuse/core"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useService } from "dioc/vue" import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
import { RESTTabService } from "~/services/tab/rest" import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const RESTTabs = useService(RESTTabService)
const GQLTabs = useService(GQLTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType = type CollectionType =
@@ -127,13 +123,13 @@ const emit = defineEmits<{
}>() }>()
const gqlRequestName = computedWithControl( const gqlRequestName = computedWithControl(
() => GQLTabs.currentActiveTab.value, () => activeGQLTab.value,
() => GQLTabs.currentActiveTab.value.document.request.name () => activeGQLTab.value.document.request.name
) )
const restRequestName = computedWithControl( const restRequestName = computedWithControl(
() => RESTTabs.currentActiveTab.value, () => activeRESTTab.value,
() => RESTTabs.currentActiveTab.value.document.request.name () => activeRESTTab.value.document.request.name
) )
const reqName = computed(() => { const reqName = computed(() => {
@@ -149,14 +145,12 @@ const reqName = computed(() => {
const requestName = ref(reqName.value) const requestName = ref(reqName.value)
watch( watch(
() => [RESTTabs.currentActiveTab.value, GQLTabs.currentActiveTab.value], () => [activeRESTTab.value, activeGQLTab.value],
() => { () => {
if (props.mode === "rest") { if (props.mode === "rest") {
requestName.value = requestName.value = activeRESTTab.value?.document.request.name ?? ""
RESTTabs.currentActiveTab.value?.document.request.name ?? ""
} else { } else {
requestName.value = requestName.value = activeGQLTab.value?.document.request.name ?? ""
GQLTabs.currentActiveTab.value?.document.request.name ?? ""
} }
} }
) )
@@ -216,8 +210,8 @@ const saveRequestAs = async () => {
const requestUpdated = const requestUpdated =
props.mode === "rest" props.mode === "rest"
? cloneDeep(RESTTabs.currentActiveTab.value.document.request) ? cloneDeep(activeRESTTab.value.document.request)
: cloneDeep(GQLTabs.currentActiveTab.value.document.request) : cloneDeep(activeGQLTab.value.document.request)
requestUpdated.name = requestName.value requestUpdated.name = requestName.value
@@ -230,7 +224,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
RESTTabs.currentActiveTab.value.document = { activeRESTTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -257,7 +251,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
RESTTabs.currentActiveTab.value.document = { activeRESTTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -285,7 +279,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
RESTTabs.currentActiveTab.value.document = { activeRESTTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -445,7 +439,7 @@ const updateTeamCollectionOrFolder = (
(result) => { (result) => {
const { createRequestInCollection } = result const { createRequestInCollection } = result
RESTTabs.currentActiveTab.value.document = { activeRESTTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -466,7 +460,7 @@ const updateTeamCollectionOrFolder = (
const requestSaved = () => { const requestSaved = () => {
toast.success(`${t("request.added")}`) toast.success(`${t("request.added")}`)
nextTick(() => { nextTick(() => {
RESTTabs.currentActiveTab.value.document.isDirty = false activeRESTTab.value.document.isDirty = false
}) })
hideModal() hideModal()
} }

View File

@@ -15,12 +15,12 @@
class="!rounded-none" class="!rounded-none"
:icon="IconPlus" :icon="IconPlus"
:title="t('team.no_access')" :title="t('team.no_access')"
:label="t('add.new')" :label="t('action.new')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
:icon="IconPlus" :icon="IconPlus"
:label="t('add.new')" :label="t('action.new')"
class="!rounded-none" class="!rounded-none"
@click="emit('display-modal-add')" @click="emit('display-modal-add')"
/> />
@@ -39,7 +39,7 @@
collectionsType.type === 'team-collections' && collectionsType.type === 'team-collections' &&
collectionsType.selectedTeam === undefined collectionsType.selectedTeam === undefined
" "
:icon="IconImport" :icon="IconArchive"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@click="emit('display-modal-import-export')" @click="emit('display-modal-import-export')"
/> />
@@ -261,68 +261,55 @@
/> />
</template> </template>
<template #emptyNode="{ node }"> <template #emptyNode="{ node }">
<HoppSmartPlaceholder <div v-if="node === null">
v-if="node === null" <div @drop="(e) => e.stopPropagation()">
:src="`/images/states/${colorMode.value}/pack.svg`" <HoppSmartPlaceholder
:alt="`${t('empty.collections')}`" :src="`/images/states/${colorMode.value}/pack.svg`"
:text="t('empty.collections')" :alt="`${t('empty.collections')}`"
@drop.stop :text="t('empty.collections')"
> >
<div class="flex flex-col items-center space-y-4"> <HoppButtonSecondary
<span class="text-secondaryLight text-center"> v-if="hasNoTeamAccess"
{{ t("collection.import_or_create") }} v-tippy="{ theme: 'tooltip' }"
</span> disabled
<div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled filled
outline outline
:disabled="hasNoTeamAccess" :title="t('team.no_access')"
:title="hasNoTeamAccess ? t('team.no_access') : ''" :label="t('action.new')"
@click="
hasNoTeamAccess ? null : emit('display-modal-import-export')
"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else
:icon="IconPlus" :icon="IconPlus"
:label="t('add.new')" :label="t('action.new')"
filled filled
outline outline
:disabled="hasNoTeamAccess" @click="emit('display-modal-add')"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/> />
</div> </HoppSmartPlaceholder>
</div> </div>
</HoppSmartPlaceholder> </div>
<HoppSmartPlaceholder <div
v-else-if="node.data.type === 'collections'" v-else-if="node.data.type === 'collections'"
:src="`/images/states/${colorMode.value}/pack.svg`" @drop="(e) => e.stopPropagation()"
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
@drop.stop
> >
<HoppButtonSecondary <HoppSmartPlaceholder
:label="t('add.new')" :src="`/images/states/${colorMode.value}/pack.svg`"
filled :alt="`${t('empty.collections')}`"
outline :text="t('empty.collections')"
@click=" >
node.data.type === 'collections' && </HoppSmartPlaceholder>
emit('add-folder', { </div>
path: node.id, <div
folder: node.data.data.data,
})
"
/>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'" v-else-if="node.data.type === 'folders'"
:src="`/images/states/${colorMode.value}/pack.svg`" @drop="(e) => e.stopPropagation()"
:alt="`${t('empty.folder')}`" >
:text="t('empty.folder')" <HoppSmartPlaceholder
@drop.stop :src="`/images/states/${colorMode.value}/pack.svg`"
/> :alt="`${t('empty.folder')}`"
:text="t('empty.folder')"
>
</HoppSmartPlaceholder>
</div>
</template> </template>
</HoppSmartTree> </HoppSmartTree>
</div> </div>
@@ -330,9 +317,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconArchive from "~icons/lucide/archive"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
import { computed, PropType, Ref, toRef } from "vue" import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
@@ -348,12 +335,10 @@ import { HoppRESTRequest } from "@hoppscotch/data"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js" import { Picked } from "~/helpers/types/HoppPicked.js"
import { RESTTabService } from "~/services/tab/rest" import { currentActiveTab } from "~/helpers/rest/tab"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const tabs = useService(RESTTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
@@ -551,7 +536,7 @@ const isSelected = ({
} }
} }
const active = computed(() => tabs.currentActiveTab.value.document.saveContext) const active = computed(() => currentActiveTab.value.document.saveContext)
const isActiveRequest = (requestID: string) => { const isActiveRequest = (requestID: string) => {
return pipe( return pipe(

View File

@@ -36,14 +36,11 @@
import { ref, watch } from "vue" import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useService } from "dioc/vue" import { currentActiveTab } from "~/helpers/graphql/tab"
import { GQLTabService } from "~/services/tab/graphql"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
const props = defineProps<{ const props = defineProps<{
show: boolean show: boolean
folderPath?: string folderPath?: string
@@ -66,7 +63,7 @@ watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
editingName.value = tabs.currentActiveTab.value?.document.request.name editingName.value = currentActiveTab.value?.document.request.name
} }
} }
) )

View File

@@ -220,8 +220,7 @@ import {
moveGraphqlRequest, moveGraphqlRequest,
} from "~/newstore/collections" } from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { useService } from "dioc/vue" import { getTabsRefTo } from "~/helpers/graphql/tab"
import { GQLTabService } from "~/services/tab/graphql"
const props = defineProps({ const props = defineProps({
picked: { type: Object, default: null }, picked: { type: Object, default: null },
@@ -236,8 +235,6 @@ const colorMode = useColorMode()
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
// TODO: improve types plz // TODO: improve types plz
const emit = defineEmits<{ const emit = defineEmits<{
(e: "select", i: Picked | null): void (e: "select", i: Picked | null): void
@@ -298,7 +295,7 @@ const removeCollection = () => {
emit("select", null) emit("select", null)
} }
const possibleTabs = tabs.getTabsRefTo((tab) => { const possibleTabs = getTabsRefTo((tab) => {
const ctx = tab.document.saveContext const ctx = tab.document.saveContext
if (!ctx) return false if (!ctx) return false

View File

@@ -203,15 +203,12 @@ import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections" import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { useService } from "dioc/vue" import { getTabsRefTo } from "~/helpers/graphql/tab"
import { GQLTabService } from "~/services/tab/graphql"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const tabs = useService(GQLTabService)
const props = defineProps({ const props = defineProps({
picked: { type: Object, default: null }, picked: { type: Object, default: null },
// Whether the request is in a selectable mode (activates 'select' event) // Whether the request is in a selectable mode (activates 'select' event)
@@ -280,7 +277,7 @@ const removeFolder = () => {
emit("select", { picked: null }) emit("select", { picked: null })
} }
const possibleTabs = tabs.getTabsRefTo((tab) => { const possibleTabs = getTabsRefTo((tab) => {
const ctx = tab.document.saveContext const ctx = tab.document.saveContext
if (!ctx) return false if (!ctx) return false

View File

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

View File

@@ -137,8 +137,12 @@ import { useToast } from "@composables/toast"
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data" import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { removeGraphqlRequest } from "~/newstore/collections" import { removeGraphqlRequest } from "~/newstore/collections"
import { useService } from "dioc/vue" import {
import { GQLTabService } from "~/services/tab/graphql" createNewTab,
getTabRefWithSaveContext,
currentTabID,
currentActiveTab,
} from "~/helpers/graphql/tab"
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
@@ -150,8 +154,6 @@ const deleteAction = ref<any | null>(null)
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(GQLTabService)
const props = defineProps({ const props = defineProps({
// Whether the object is selected (show the tick mark) // Whether the object is selected (show the tick mark)
picked: { type: Object, default: null }, picked: { type: Object, default: null },
@@ -163,7 +165,7 @@ const props = defineProps({
}) })
const isActive = computed(() => { const isActive = computed(() => {
const saveCtx = tabs.currentActiveTab.value?.document.saveContext const saveCtx = currentActiveTab.value?.document.saveContext
if (!saveCtx) return false if (!saveCtx) return false
@@ -199,7 +201,7 @@ const selectRequest = () => {
if (props.saveRequest) { if (props.saveRequest) {
pick() pick()
} else { } else {
const possibleTab = tabs.getTabRefWithSaveContext({ const possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath: props.folderPath, folderPath: props.folderPath,
requestIndex: props.requestIndex, requestIndex: props.requestIndex,
@@ -207,11 +209,11 @@ const selectRequest = () => {
// Switch to that request if that request is open // Switch to that request if that request is open
if (possibleTab) { if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id) currentTabID.value = possibleTab.value.id
return return
} }
tabs.createNewTab({ createNewTab({
saveContext: { saveContext: {
originLocation: "user-collection", originLocation: "user-collection",
folderPath: props.folderPath, folderPath: props.folderPath,
@@ -251,7 +253,7 @@ const removeRequest = () => {
} }
// Detach the request from any of the tabs // Detach the request from any of the tabs
const possibleTab = tabs.getTabRefWithSaveContext({ const possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath: props.folderPath, folderPath: props.folderPath,
requestIndex: props.requestIndex, requestIndex: props.requestIndex,

View File

@@ -34,7 +34,7 @@
v-if="!saveRequest" v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('modal.import_export')" :title="t('modal.import_export')"
:icon="IconImport" :icon="IconArchive"
@click="displayModalImportExport(true)" @click="displayModalImportExport(true)"
/> />
</div> </div>
@@ -66,27 +66,12 @@
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
> >
<div class="flex flex-col items-center space-y-4"> <HoppButtonSecondary
<span class="text-secondaryLight text-center"> :label="t('add.new')"
{{ t("collection.import_or_create") }} filled
</span> outline
<div class="flex gap-4 flex-col items-stretch"> @click="displayModalAdd(true)"
<HoppButtonPrimary />
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="!(filteredCollections.length !== 0 || collections.length === 0)" v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
@@ -155,13 +140,12 @@ import {
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down" import IconArchive from "~icons/lucide/archive"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useService } from "dioc/vue" import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
import { GQLTabService } from "~/services/tab/graphql"
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -174,16 +158,14 @@ export default defineComponent({
const collections = useReadonlyStream(graphqlCollections$, [], "deep") const collections = useReadonlyStream(graphqlCollections$, [], "deep")
const colorMode = useColorMode() const colorMode = useColorMode()
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
return { return {
collections, collections,
colorMode, colorMode,
t, t,
tabs,
IconPlus, IconPlus,
IconHelpCircle, IconHelpCircle,
IconImport, IconArchive,
} }
}, },
data() { data() {
@@ -285,13 +267,13 @@ export default defineComponent({
}, },
onAddRequest({ name, path, index }) { onAddRequest({ name, path, index }) {
const newRequest = { const newRequest = {
...this.tabs.currentActiveTab.value.document.request, ...currentActiveTab.value.document.request,
name, name,
} }
saveGraphqlRequestAs(path, newRequest) saveGraphqlRequestAs(path, newRequest)
this.tabs.createNewTab({ createNewTab({
saveContext: { saveContext: {
originLocation: "user-collection", originLocation: "user-collection",
folderPath: path, folderPath: path,

View File

@@ -18,12 +18,13 @@
" "
> >
<WorkspaceCurrent :section="t('tab.collections')" /> <WorkspaceCurrent :section="t('tab.collections')" />
<input
<HoppSmartInput
v-model="filterTexts" v-model="filterTexts"
type="search"
autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8"
:placeholder="t('action.search')" :placeholder="t('action.search')"
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
type="search"
:autofocus="false"
:disabled="collectionsType.type === 'team-collections'" :disabled="collectionsType.type === 'team-collections'"
/> />
</div> </div>
@@ -218,6 +219,12 @@ import {
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { platform } from "~/platform" import { platform } from "~/platform"
import { createCollectionGists } from "~/helpers/gist" import { createCollectionGists } from "~/helpers/gist"
import {
createNewTab,
currentActiveTab,
currentTabID,
getTabRefWithSaveContext,
} from "~/helpers/rest/tab"
import { import {
getRequestsByPath, getRequestsByPath,
resolveSaveContextOnRequestReorder, resolveSaveContextOnRequestReorder,
@@ -232,11 +239,9 @@ import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const props = defineProps({ const props = defineProps({
saveRequest: { saveRequest: {
@@ -372,26 +377,22 @@ const updateSelectedTeam = (team: SelectedTeam) => {
const workspace = workspaceService.currentWorkspace const workspace = workspaceService.currentWorkspace
// Used to switch collection type and team when user switch workspace in the global workspace switcher // Used to switch collection type and team when user switch workspace in the global workspace switcher
// Check if there is a teamID in the workspace, if yes, switch to team collections and select the team // Check if there is a teamID in the workspace, if yes, switch to team collection and select the team
// If there is no teamID, switch to my collections // If there is no teamID, switch to my environment
watch( watch(
() => { () => {
const space = workspace.value const space = workspace.value
return space.type === "personal" ? undefined : space.teamID
if (space.type === "personal") return undefined
else return space.teamID
}, },
(teamID) => { (teamID) => {
if (teamID) { if (!teamID) {
switchToMyCollections()
} else if (teamID) {
const team = myTeams.value?.find((t) => t.id === teamID) const team = myTeams.value?.find((t) => t.id === teamID)
if (team) { if (team) updateSelectedTeam(team)
updateSelectedTeam(team)
}
return
} }
return switchToMyCollections()
},
{
immediate: true,
} }
) )
@@ -649,7 +650,7 @@ const addRequest = (payload: {
const onAddRequest = (requestName: string) => { const onAddRequest = (requestName: string) => {
const newRequest = { const newRequest = {
...cloneDeep(tabs.currentActiveTab.value.document.request), ...cloneDeep(currentActiveTab.value.document.request),
name: requestName, name: requestName,
} }
@@ -658,7 +659,7 @@ const onAddRequest = (requestName: string) => {
if (!path) return if (!path) return
const insertionIndex = saveRESTRequestAs(path, newRequest) const insertionIndex = saveRESTRequestAs(path, newRequest)
tabs.createNewTab({ createNewTab({
request: newRequest, request: newRequest,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -707,7 +708,7 @@ const onAddRequest = (requestName: string) => {
(result) => { (result) => {
const { createRequestInCollection } = result const { createRequestInCollection } = result
tabs.createNewTab({ createNewTab({
request: newRequest, request: newRequest,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -930,7 +931,7 @@ const updateEditingRequest = (newName: string) => {
if (folderPath === null || requestIndex === null) return if (folderPath === null || requestIndex === null) return
const possibleActiveTab = tabs.getTabRefWithSaveContext({ const possibleActiveTab = getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
requestIndex, requestIndex,
folderPath, folderPath,
@@ -974,7 +975,7 @@ const updateEditingRequest = (newName: string) => {
) )
)() )()
const possibleTab = tabs.getTabRefWithSaveContext({ const possibleTab = getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID, requestID,
}) })
@@ -1210,7 +1211,7 @@ const onRemoveRequest = () => {
emit("select", null) emit("select", null)
} }
const possibleTab = tabs.getTabRefWithSaveContext({ const possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath, folderPath,
requestIndex, requestIndex,
@@ -1270,7 +1271,7 @@ const onRemoveRequest = () => {
)() )()
// If there is a tab attached to this request, dissociate its state and mark it dirty // If there is a tab attached to this request, dissociate its state and mark it dirty
const possibleTab = tabs.getTabRefWithSaveContext({ const possibleTab = getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID, requestID,
}) })
@@ -1303,14 +1304,14 @@ const selectRequest = (selectedRequest: {
let possibleTab = null let possibleTab = null
if (collectionsType.value.type === "team-collections") { if (collectionsType.value.type === "team-collections") {
possibleTab = tabs.getTabRefWithSaveContext({ possibleTab = getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
}) })
if (possibleTab) { if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id) currentTabID.value = possibleTab.value.id
} else { } else {
tabs.createNewTab({ createNewTab({
request: cloneDeep(request), request: cloneDeep(request),
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -1320,16 +1321,16 @@ const selectRequest = (selectedRequest: {
}) })
} }
} else { } else {
possibleTab = tabs.getTabRefWithSaveContext({ possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
requestIndex: parseInt(requestIndex), requestIndex: parseInt(requestIndex),
folderPath: folderPath!, folderPath: folderPath!,
}) })
if (possibleTab) { if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id) currentTabID.value = possibleTab.value.id
} else { } else {
// If not, open the request in a new tab // If not, open the request in a new tab
tabs.createNewTab({ createNewTab({
request: cloneDeep(request), request: cloneDeep(request),
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -1372,7 +1373,7 @@ const dropRequest = (payload: {
destinationCollectionIndex destinationCollectionIndex
) )
const possibleTab = tabs.getTabRefWithSaveContext({ const possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath, folderPath,
requestIndex: pathToLastIndex(requestIndex), requestIndex: pathToLastIndex(requestIndex),
@@ -1421,7 +1422,7 @@ const dropRequest = (payload: {
1 1
) )
const possibleTab = tabs.getTabRefWithSaveContext({ const possibleTab = getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
}) })
@@ -1866,25 +1867,28 @@ const getJSONCollection = async () => {
* @param collectionJSON - JSON string of the collection * @param collectionJSON - JSON string of the collection
* @param name - Name of the collection set as the file name * @param name - Name of the collection set as the file name
*/ */
const initializeDownloadCollection = async ( const initializeDownloadCollection = (
collectionJSON: string, collectionJSON: string,
name: string | null name: string | null
) => { ) => {
const result = await platform.io.saveFileWithDialog({ const file = new Blob([collectionJSON], { type: "application/json" })
data: collectionJSON, const a = document.createElement("a")
contentType: "application/json", const url = URL.createObjectURL(file)
suggestedFilename: `${name ?? "collection"}.json`, a.href = url
filters: [
{
name: "Hoppscotch Collection JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") { if (name) {
toast.success(t("state.download_started").toString()) a.download = `${name}.json`
} else {
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
} }
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
} }
/** /**
@@ -1913,14 +1917,11 @@ const exportData = async (
exportLoading.value = false exportLoading.value = false
return return
}, },
async (coll) => { (coll) => {
const hoppColl = teamCollToHoppRESTColl(coll) const hoppColl = teamCollToHoppRESTColl(coll)
const collectionJSONString = JSON.stringify(hoppColl) const collectionJSONString = JSON.stringify(hoppColl)
await initializeDownloadCollection( initializeDownloadCollection(collectionJSONString, hoppColl.name)
collectionJSONString,
hoppColl.name
)
exportLoading.value = false exportLoading.value = false
} }
) )
@@ -1937,12 +1938,6 @@ const exportJSONCollection = async () => {
await getJSONCollection() await getJSONCollection()
const parsedCollections = JSON.parse(collectionJSON.value)
if (!parsedCollections.length) {
return toast.error(t("error.no_collections_to_export"))
}
initializeDownloadCollection(collectionJSON.value, null) initializeDownloadCollection(collectionJSON.value, null)
} }

View File

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

View File

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

View File

@@ -83,14 +83,11 @@ import {
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment" import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { RESTTabService } from "~/services/tab/rest" import { currentActiveTab } from "~/helpers/rest/tab"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const props = defineProps<{ const props = defineProps<{
show: boolean show: boolean
position: { top: number; left: number } position: { top: number; left: number }
@@ -192,8 +189,8 @@ const addEnvironment = async () => {
//replace the current tab endpoint with the variable name with << and >> //replace the current tab endpoint with the variable name with << and >>
const variableName = `<<${editingName.value}>>` const variableName = `<<${editingName.value}>>`
//replace the currenttab endpoint containing the value in the text with variablename //replace the currenttab endpoint containing the value in the text with variablename
tabs.currentActiveTab.value.document.request.endpoint = currentActiveTab.value.document.request.endpoint =
tabs.currentActiveTab.value.document.request.endpoint.replace( currentActiveTab.value.document.request.endpoint.replace(
editingValue.value, editingValue.value,
variableName variableName
) )

View File

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

View File

@@ -66,7 +66,7 @@
/> />
<HoppSmartTabs <HoppSmartTabs
v-model="selectedEnvTab" v-model="selectedEnvTab"
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary ${ :styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
!isTeamSelected || workspace.type === 'personal' !isTeamSelected || workspace.type === 'personal'
? 'bg-primaryLight' ? 'bg-primaryLight'
: '' : ''
@@ -478,8 +478,7 @@ watch(
teamEnvListAdapter.changeTeamID(newVal.teamID) teamEnvListAdapter.changeTeamID(newVal.teamID)
} }
} }
}, }
{ immediate: true }
) )
const selectedEnv = computed(() => { const selectedEnv = computed(() => {

View File

@@ -60,7 +60,17 @@
:placeholder="`${t('count.value', { count: index + 1 })}`" :placeholder="`${t('count.value', { count: index + 1 })}`"
:envs="liveEnvs" :envs="liveEnvs"
:name="'value' + index" :name="'value' + index"
:secret="env.secret"
/> />
<div class="flex">
<HoppButtonSecondary
id="variable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.secret')"
:icon="env.secret ? IconEyeOff : IconEye"
@click="toggleEnvironmentSecret(index)"
/>
</div>
<div class="flex"> <div class="flex">
<HoppButtonSecondary <HoppButtonSecondary
id="variable" id="variable"
@@ -110,6 +120,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
import IconDone from "~icons/lucide/check" import IconDone from "~icons/lucide/check"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconTrash from "~icons/lucide/trash" import IconTrash from "~icons/lucide/trash"
import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import { clone } from "lodash-es" import { clone } from "lodash-es"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
@@ -140,6 +152,7 @@ type EnvironmentVariable = {
env: { env: {
key: string key: string
value: string value: string
secret: boolean
} }
} }
@@ -172,7 +185,7 @@ const idTicker = ref(0)
const editingName = ref<string | null>(null) const editingName = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([ const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } }, { id: idTicker.value++, env: { key: "", value: "", secret: false } },
]) ])
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>( const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
@@ -203,6 +216,8 @@ const workingEnv = computed(() => {
} }
}) })
const oldEnvironments = ref<string[]>([])
const envList = useReadonlyStream(environments$, []) || props.envVars() const envList = useReadonlyStream(environments$, []) || props.envVars()
const evnExpandError = computed(() => { const evnExpandError = computed(() => {
@@ -234,6 +249,28 @@ const liveEnvs = computed(() => {
} }
}) })
watch(liveEnvs, (newLiveEnv, oldLiveEnv) => {
const oldEnvLength = oldLiveEnv.length
const newEnvLength = newLiveEnv.length
if (oldEnvLength === newEnvLength) {
const _oldEnvironments = []
for (let i = 0; i < newEnvLength; i++) {
const newVar = newLiveEnv[i]
const oldVar = oldLiveEnv[i]
let newValue = ""
if (!newVar.secret) {
newValue = newVar.value
} else if (!oldVar.secret) {
newValue = oldVar.value
} else {
newValue = oldEnvironments.value[i]
}
_oldEnvironments.push(newValue)
}
oldEnvironments.value = _oldEnvironments
}
})
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
@@ -262,6 +299,7 @@ const addEnvironmentVariable = () => {
env: { env: {
key: "", key: "",
value: "", value: "",
secret: false,
}, },
}) })
} }
@@ -270,12 +308,23 @@ const removeEnvironmentVariable = (index: number) => {
vars.value.splice(index, 1) vars.value.splice(index, 1)
} }
const toggleEnvironmentSecret = (index: number) => {
vars.value[index].env.secret = !vars.value[index].env.secret
}
const saveEnvironment = () => { const saveEnvironment = () => {
if (!editingName.value) { if (!editingName.value) {
toast.error(`${t("environment.invalid_name")}`) toast.error(`${t("environment.invalid_name")}`)
return return
} }
const _vars = vars.value
for (let i = 0; i < vars.value.length; i++) {
const value = oldEnvironments.value[i]
if (value) {
_vars[i].env.value = value
}
}
vars.value = _vars
const filterdVariables = pipe( const filterdVariables = pipe(
vars.value, vars.value,
A.filterMap( A.filterMap(

View File

@@ -46,7 +46,6 @@
role="menu" role="menu"
@keyup.e="edit!.$el.click()" @keyup.e="edit!.$el.click()"
@keyup.d="duplicate!.$el.click()" @keyup.d="duplicate!.$el.click()"
@keyup.j="exportAsJsonEl!.$el.click()"
@keyup.delete=" @keyup.delete="
!(environmentIndex === 'Global') !(environmentIndex === 'Global')
? deleteAction!.$el.click() ? deleteAction!.$el.click()
@@ -78,18 +77,6 @@
} }
" "
/> />
<HoppSmartItem
ref="exportAsJsonEl"
:icon="IconEdit"
:label="`${t('export.as_json')}`"
:shortcut="['J']"
@click="
() => {
exportEnvironmentAsJSON()
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
v-if="environmentIndex !== 'Global'" v-if="environmentIndex !== 'Global'"
ref="deleteAction" ref="deleteAction"
@@ -134,7 +121,6 @@ import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { HoppSmartItem } from "@hoppscotch/ui" import { HoppSmartItem } from "@hoppscotch/ui"
import { exportAsJSON } from "~/helpers/import-export/export/environment"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -150,18 +136,10 @@ const emit = defineEmits<{
const confirmRemove = ref(false) const confirmRemove = ref(false)
const exportEnvironmentAsJSON = () => {
const { environment, environmentIndex } = props
exportAsJSON(environment, environmentIndex)
? toast.success(t("state.download_started"))
: toast.error(t("state.download_failed"))
}
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const edit = ref<typeof HoppSmartItem>() const edit = ref<typeof HoppSmartItem>()
const duplicate = ref<typeof HoppSmartItem>() const duplicate = ref<typeof HoppSmartItem>()
const exportAsJsonEl = ref<typeof HoppSmartItem>()
const deleteAction = ref<typeof HoppSmartItem>() const deleteAction = ref<typeof HoppSmartItem>()
const removeEnvironment = () => { const removeEnvironment = () => {

View File

@@ -19,7 +19,7 @@
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconImport" :icon="IconArchive"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@click="displayModalImportExport(true)" @click="displayModalImportExport(true)"
/> />
@@ -33,32 +33,17 @@
@edit-environment="editEnvironment(index)" @edit-environment="editEnvironment(index)"
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="!environments.length" v-if="environments.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')" :text="t('empty.environments')"
> >
<div class="flex flex-col items-center space-y-4"> <HoppButtonSecondary
<span class="text-secondaryLight text-center"> :label="`${t('add.new')}`"
{{ t("environment.import_or_create") }} filled
</span> outline
<div class="flex gap-4 flex-col items-stretch"> @click="displayModalAdd(true)"
<HoppButtonPrimary />
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="`${t('add.new')}`"
filled
outline
@click="displayModalAdd(true)"
/>
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<EnvironmentsMyDetails <EnvironmentsMyDetails
:show="showModalDetails" :show="showModalDetails"
@@ -81,8 +66,8 @@ import { environments$ } from "~/newstore/environments"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import IconArchive from "~icons/lucide/archive"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconImport from "~icons/lucide/folder-down"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import { Environment } from "@hoppscotch/data" import { Environment } from "@hoppscotch/data"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"

View File

@@ -63,7 +63,17 @@
:envs="liveEnvs" :envs="liveEnvs"
:name="'value' + index" :name="'value' + index"
:readonly="isViewer" :readonly="isViewer"
:secret="env.secret"
/> />
<div v-if="!isViewer" class="flex">
<HoppButtonSecondary
id="variable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.secret')"
:icon="env.secret ? IconEyeOff : IconEye"
@click="toggleEnvironmentSecret(index)"
/>
</div>
<div v-if="!isViewer" class="flex"> <div v-if="!isViewer" class="flex">
<HoppButtonSecondary <HoppButtonSecondary
id="variable" id="variable"
@@ -139,6 +149,8 @@ import IconTrash from "~icons/lucide/trash"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconDone from "~icons/lucide/check" import IconDone from "~icons/lucide/check"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import { platform } from "~/platform" import { platform } from "~/platform"
type EnvironmentVariable = { type EnvironmentVariable = {
@@ -146,6 +158,7 @@ type EnvironmentVariable = {
env: { env: {
key: string key: string
value: string value: string
secret: boolean
} }
} }
@@ -182,9 +195,10 @@ const idTicker = ref(0)
const editingName = ref<string | null>(null) const editingName = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([ const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } }, { id: idTicker.value++, env: { key: "", value: "", secret: false } },
]) ])
const oldEnvironments = ref<string[]>([])
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>( const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
IconTrash2, IconTrash2,
1000 1000
@@ -212,6 +226,28 @@ const liveEnvs = computed(() => {
} }
}) })
watch(liveEnvs, (newLiveEnv, oldLiveEnv) => {
const oldEnvLength = oldLiveEnv.length
const newEnvLength = newLiveEnv.length
if (oldEnvLength === newEnvLength) {
const _oldEnvironments = []
for (let i = 0; i < newEnvLength; i++) {
const newVar = newLiveEnv[i]
const oldVar = oldLiveEnv[i]
let newValue = ""
if (!newVar.secret) {
newValue = newVar.value
} else if (!oldVar.secret) {
newValue = oldVar.value
} else {
newValue = oldEnvironments.value[i]
}
_oldEnvironments.push(newValue)
}
oldEnvironments.value = _oldEnvironments
}
})
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
@@ -220,7 +256,7 @@ watch(
editingName.value = null editingName.value = null
vars.value = pipe( vars.value = pipe(
props.envVars() ?? [], props.envVars() ?? [],
A.map((e: { key: string; value: string }) => ({ A.map((e: { key: string; value: string; secret: boolean }) => ({
id: idTicker.value++, id: idTicker.value++,
env: clone(e), env: clone(e),
})) }))
@@ -229,7 +265,7 @@ watch(
editingName.value = props.editingEnvironment.environment.name ?? null editingName.value = props.editingEnvironment.environment.name ?? null
vars.value = pipe( vars.value = pipe(
props.editingEnvironment.environment.variables ?? [], props.editingEnvironment.environment.variables ?? [],
A.map((e: { key: string; value: string }) => ({ A.map((e: { key: string; value: string; secret: boolean }) => ({
id: idTicker.value++, id: idTicker.value++,
env: clone(e), env: clone(e),
})) }))
@@ -251,6 +287,7 @@ const addEnvironmentVariable = () => {
env: { env: {
key: "", key: "",
value: "", value: "",
secret: false,
}, },
}) })
} }
@@ -259,6 +296,10 @@ const removeEnvironmentVariable = (index: number) => {
vars.value.splice(index, 1) vars.value.splice(index, 1)
} }
const toggleEnvironmentSecret = (index: number) => {
vars.value[index].env.secret = !vars.value[index].env.secret
}
const isLoading = ref(false) const isLoading = ref(false)
const saveEnvironment = async () => { const saveEnvironment = async () => {
@@ -269,6 +310,15 @@ const saveEnvironment = async () => {
return return
} }
const _vars = vars.value
for (let i = 0; i < vars.value.length; i++) {
const value = oldEnvironments.value[i]
if (value) {
_vars[i].env.value = value
}
}
vars.value = _vars
const filterdVariables = pipe( const filterdVariables = pipe(
vars.value, vars.value,
A.filterMap( A.filterMap(

View File

@@ -39,7 +39,6 @@
role="menu" role="menu"
@keyup.e="edit!.$el.click()" @keyup.e="edit!.$el.click()"
@keyup.d="duplicate!.$el.click()" @keyup.d="duplicate!.$el.click()"
@keyup.j="exportAsJsonEl!.$el.click()"
@keyup.delete="deleteAction!.$el.click()" @keyup.delete="deleteAction!.$el.click()"
@keyup.escape="options!.tippy().hide()" @keyup.escape="options!.tippy().hide()"
> >
@@ -55,7 +54,6 @@
} }
" "
/> />
<HoppSmartItem <HoppSmartItem
ref="duplicate" ref="duplicate"
:icon="IconCopy" :icon="IconCopy"
@@ -68,18 +66,6 @@
} }
" "
/> />
<HoppSmartItem
ref="exportAsJsonEl"
:icon="IconEdit"
:label="`${t('export.as_json')}`"
:shortcut="['J']"
@click="
() => {
exportEnvironmentAsJSON()
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
ref="deleteAction" ref="deleteAction"
:icon="IconTrash2" :icon="IconTrash2"
@@ -123,7 +109,6 @@ import IconTrash2 from "~icons/lucide/trash-2"
import IconMoreVertical from "~icons/lucide/more-vertical" import IconMoreVertical from "~icons/lucide/more-vertical"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { HoppSmartItem } from "@hoppscotch/ui" import { HoppSmartItem } from "@hoppscotch/ui"
import { exportAsJSON } from "~/helpers/import-export/export/environment"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -139,17 +124,11 @@ const emit = defineEmits<{
const confirmRemove = ref(false) const confirmRemove = ref(false)
const exportEnvironmentAsJSON = () =>
exportAsJSON(props.environment)
? toast.success(t("state.download_started"))
: toast.error(t("state.download_failed"))
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const edit = ref<typeof HoppSmartItem>() const edit = ref<typeof HoppSmartItem>()
const duplicate = ref<typeof HoppSmartItem>() const duplicate = ref<typeof HoppSmartItem>()
const deleteAction = ref<typeof HoppSmartItem>() const deleteAction = ref<typeof HoppSmartItem>()
const exportAsJsonEl = ref<typeof HoppSmartItem>()
const removeEnvironment = () => { const removeEnvironment = () => {
pipe( pipe(

View File

@@ -31,49 +31,40 @@
v-if="team !== undefined && team.myRole === 'VIEWER'" v-if="team !== undefined && team.myRole === 'VIEWER'"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
disabled disabled
:icon="IconImport" :icon="IconArchive"
:title="t('modal.import_export')" :title="t('modal.import_export')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconImport" :icon="IconArchive"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@click="displayModalImportExport(true)" @click="displayModalImportExport(true)"
/> />
</div> </div>
</div> </div>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="!loading && !teamEnvironments.length && !adapterError" v-if="!loading && teamEnvironments.length === 0 && !adapterError"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')" :text="t('empty.environments')"
> >
<div class="flex flex-col items-center space-y-4"> <HoppButtonSecondary
<span class="text-secondaryLight text-center"> v-if="team === undefined || team.myRole === 'VIEWER'"
{{ t("environment.import_or_create") }} v-tippy="{ theme: 'tooltip' }"
</span> disabled
<div class="flex gap-4 flex-col items-stretch"> filled
<HoppButtonPrimary :icon="IconPlus"
:icon="IconImport" :title="t('team.no_access')"
:label="t('import.title')" :label="t('action.new')"
filled />
outline <HoppButtonSecondary
:title="isTeamViewer ? t('team.no_access') : ''" v-else
:disabled="isTeamViewer" :label="`${t('add.new')}`"
@click="isTeamViewer ? null : displayModalImportExport(true)" filled
/> outline
<HoppButtonSecondary @click="displayModalAdd(true)"
:label="`${t('add.new')}`" />
filled
outline
:icon="IconPlus"
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalAdd(true)"
/>
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div v-else-if="!loading"> <div v-else-if="!loading">
<EnvironmentsTeamsEnvironment <EnvironmentsTeamsEnvironment
@@ -117,14 +108,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue" import { ref } from "vue"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment" import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconArchive from "~icons/lucide/archive"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
@@ -147,8 +138,6 @@ const action = ref<"new" | "edit">("edit")
const editingEnvironment = ref<TeamEnvironment | null>(null) const editingEnvironment = ref<TeamEnvironment | null>(null)
const editingVariableName = ref("") const editingVariableName = ref("")
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
const displayModalAdd = (shouldDisplay: boolean) => { const displayModalAdd = (shouldDisplay: boolean) => {
action.value = "new" action.value = "new"
showModalDetails.value = shouldDisplay showModalDetails.value = shouldDisplay

View File

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

View File

@@ -64,6 +64,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { platform } from "~/platform" import { platform } from "~/platform"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { currentActiveTab } from "~/helpers/graphql/tab"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { connection } from "~/helpers/graphql/connection" import { connection } from "~/helpers/graphql/connection"
import { connect } from "~/helpers/graphql/connection" import { connect } from "~/helpers/graphql/connection"
@@ -71,10 +72,8 @@ import { disconnect } from "~/helpers/graphql/connection"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
const interceptorService = useService(InterceptorService) const interceptorService = useService(InterceptorService)
@@ -83,9 +82,9 @@ const connectionSwitchModal = ref(false)
const connected = computed(() => connection.state === "CONNECTED") const connected = computed(() => connection.state === "CONNECTED")
const url = computed({ const url = computed({
get: () => tabs.currentActiveTab.value?.document.request.url ?? "", get: () => currentActiveTab.value?.document.request.url ?? "",
set: (value) => { set: (value) => {
tabs.currentActiveTab.value!.document.request.url = value currentActiveTab.value!.document.request.url = value
}, },
}) })
@@ -98,7 +97,7 @@ const onConnectClick = () => {
} }
const gqlConnect = () => { const gqlConnect = () => {
connect(url.value, tabs.currentActiveTab.value?.document.request.headers) connect(url.value, currentActiveTab.value?.document.request.headers)
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN", type: "HOPP_REQUEST_RUN",
@@ -115,7 +114,7 @@ const switchConnection = () => {
const lastTwoUrls = ref<string[]>([]) const lastTwoUrls = ref<string[]>([])
watch( watch(
tabs.currentActiveTab, currentActiveTab,
(newVal) => { (newVal) => {
if (newVal) { if (newVal) {
lastTwoUrls.value.push(newVal.document.request.url) lastTwoUrls.value.push(newVal.document.request.url)

View File

@@ -58,7 +58,8 @@ import { computed, ref, watch } from "vue"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { HoppGQLRequest } from "@hoppscotch/data" import { HoppGQLRequest } from "@hoppscotch/data"
import { platform } from "~/platform" import { platform } from "~/platform"
import { computedWithControl, useVModel } from "@vueuse/core" import { currentActiveTab } from "~/helpers/graphql/tab"
import { computedWithControl } from "@vueuse/core"
import { import {
GQLResponseEvent, GQLResponseEvent,
runGQLOperation, runGQLOperation,
@@ -67,39 +68,26 @@ import {
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections" import { editGraphqlRequest } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql"
const VALID_GQL_OPERATIONS = [
"query",
"headers",
"variables",
"authorization",
] as const
export type GQLOptionTabs = (typeof VALID_GQL_OPERATIONS)[number]
export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization"
const selectedOptionTab = ref<GQLOptionTabs>("query")
const interceptorService = useService(InterceptorService) const interceptorService = useService(InterceptorService)
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(GQLTabService)
// v-model integration with props and emit // v-model integration with props and emit
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue: HoppGQLRequest modelValue: HoppGQLRequest
response?: GQLResponseEvent[] | null response?: GQLResponseEvent[] | null
optionTab?: GQLOptionTabs
tabId: string tabId: string
}>(), }>(),
{ {
response: null, response: null,
optionTab: "query",
} }
) )
const emit = defineEmits(["update:modelValue", "update:response"]) const emit = defineEmits(["update:modelValue", "update:response"])
const selectedOptionTab = useVModel(props, "optionTab", emit)
const request = ref(props.modelValue) const request = ref(props.modelValue)
@@ -112,8 +100,8 @@ watch(
) )
const url = computedWithControl( const url = computedWithControl(
() => tabs.currentActiveTab.value, () => currentActiveTab.value,
() => tabs.currentActiveTab.value.document.request.url () => currentActiveTab.value.document.request.url
) )
const activeGQLHeadersCount = computed( const activeGQLHeadersCount = computed(
@@ -148,9 +136,6 @@ const runQuery = async (
const duration = Date.now() - startTime const duration = Date.now() - startTime
completePageProgress() completePageProgress()
toast.success(`${t("state.finished_in", { duration })}`) toast.success(`${t("state.finished_in", { duration })}`)
if (definition?.operation === "subscription" && request.value.auth) {
toast.success(t("authorization.graphql_headers"))
}
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
// response.value = [`${e}`] // response.value = [`${e}`]
@@ -197,17 +182,17 @@ const hideRequestModal = () => {
} }
const saveRequest = () => { const saveRequest = () => {
if ( if (
tabs.currentActiveTab.value.document.saveContext && currentActiveTab.value.document.saveContext &&
tabs.currentActiveTab.value.document.saveContext.originLocation === currentActiveTab.value.document.saveContext.originLocation ===
"user-collection" "user-collection"
) { ) {
editGraphqlRequest( editGraphqlRequest(
tabs.currentActiveTab.value.document.saveContext.folderPath, currentActiveTab.value.document.saveContext.folderPath,
tabs.currentActiveTab.value.document.saveContext.requestIndex, currentActiveTab.value.document.saveContext.requestIndex,
tabs.currentActiveTab.value.document.request currentActiveTab.value.document.request
) )
tabs.currentActiveTab.value.document.isDirty = false currentActiveTab.value.document.isDirty = false
} else { } else {
showSaveRequestModal.value = true showSaveRequestModal.value = true
} }

View File

@@ -3,13 +3,12 @@
<template #primary> <template #primary>
<GraphqlRequestOptions <GraphqlRequestOptions
v-model="tab.document.request" v-model="tab.document.request"
v-model:response="tab.document.response" v-model:response="tab.response"
v-model:option-tab="tab.document.optionTabPreference"
:tab-id="tab.id" :tab-id="tab.id"
/> />
</template> </template>
<template #secondary> <template #secondary>
<GraphqlResponse :response="tab.document.response" /> <GraphqlResponse :response="tab.response" />
</template> </template>
</AppPaneLayout> </AppPaneLayout>
</template> </template>
@@ -19,15 +18,14 @@ import { useVModel } from "@vueuse/core"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { watch } from "vue" import { watch } from "vue"
import { isEqualHoppGQLRequest } from "~/helpers/graphql" import { isEqualHoppGQLRequest } from "~/helpers/graphql"
import { HoppGQLDocument } from "~/helpers/graphql/document" import { HoppGQLTab } from "~/helpers/graphql/tab"
import { HoppTab } from "~/services/tab"
// TODO: Move Response and Request execution code to over here // TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppTab<HoppGQLDocument> }>() const props = defineProps<{ modelValue: HoppGQLTab }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", val: HoppTab<HoppGQLDocument>): void (e: "update:modelValue", val: HoppGQLTab): void
}>() }>()
const tab = useVModel(props, "modelValue", emit) const tab = useVModel(props, "modelValue", emit)

View File

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

View File

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

View File

@@ -92,13 +92,12 @@ import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square" import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit" import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import { HoppTab } from "~/services/tab" import { HoppGQLTab } from "~/helpers/graphql/tab"
import { HoppGQLDocument } from "~/helpers/graphql/document"
const t = useI18n() const t = useI18n()
defineProps<{ defineProps<{
tab: HoppTab<HoppGQLDocument> tab: HoppGQLTab
isRemovable: boolean isRemovable: boolean
}>() }>()

View File

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

View File

@@ -67,11 +67,9 @@ import IconMaximize2 from "~icons/lucide/maximize-2"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { makeGQLRequest } from "@hoppscotch/data" import { makeGQLRequest } from "@hoppscotch/data"
import { useService } from "dioc/vue" import { createNewTab } from "~/helpers/graphql/tab"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
const props = defineProps<{ const props = defineProps<{
entry: GQLHistoryEntry entry: GQLHistoryEntry
@@ -95,7 +93,7 @@ const query = computed(() =>
) )
const useEntry = () => { const useEntry = () => {
tabs.createNewTab({ createNewTab({
request: makeGQLRequest({ request: makeGQLRequest({
name: props.entry.request.name, name: props.entry.request.name,
url: props.entry.request.url, url: props.entry.request.url,

View File

@@ -9,7 +9,7 @@
v-model="filterText" v-model="filterText"
type="search" type="search"
autocomplete="off" autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8" class="flex flex-1 p-4 py-2 bg-transparent"
:placeholder="`${t('action.search')}`" :placeholder="`${t('action.search')}`"
/> />
<div class="flex"> <div class="flex">
@@ -176,9 +176,8 @@ import {
import HistoryRestCard from "./rest/Card.vue" import HistoryRestCard from "./rest/Card.vue"
import HistoryGraphqlCard from "./graphql/Card.vue" import HistoryGraphqlCard from "./graphql/Card.vue"
import { createNewTab } from "~/helpers/rest/tab"
import { defineActionHandler, invokeAction } from "~/helpers/actions" import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
@@ -294,9 +293,8 @@ const clearHistory = () => {
// NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry // NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry
// (That is not a really good behaviour tho ¯\_(ツ)_/¯) // (That is not a really good behaviour tho ¯\_(ツ)_/¯)
const tabs = useService(RESTTabService)
const useHistory = (entry: RESTHistoryEntry) => { const useHistory = (entry: RESTHistoryEntry) => {
tabs.createNewTab({ createNewTab({
request: entry.request, request: entry.request,
isDirty: false, isDirty: false,
}) })

View File

@@ -59,9 +59,7 @@
:key="`contentTypeItem-${contentTypeIndex}`" :key="`contentTypeItem-${contentTypeIndex}`"
:label="contentTypeItem" :label="contentTypeItem"
:info-icon=" :info-icon="
contentTypeItem === body.contentType contentTypeItem === body.contentType ? IconDone : null
? IconDone
: undefined
" "
:active-info-icon="contentTypeItem === body.contentType" :active-info-icon="contentTypeItem === body.contentType"
@click=" @click="
@@ -138,7 +136,7 @@ import IconDone from "~icons/lucide/check"
import IconExternalLink from "~icons/lucide/external-link" import IconExternalLink from "~icons/lucide/external-link"
import IconInfo from "~icons/lucide/info" import IconInfo from "~icons/lucide/info"
import IconRefreshCW from "~icons/lucide/refresh-cw" import IconRefreshCW from "~icons/lucide/refresh-cw"
import { RESTOptionTabs } from "./RequestOptions.vue" import { RequestOptionTabs } from "./RequestOptions.vue"
const colorMode = useColorMode() const colorMode = useColorMode()
const t = useI18n() const t = useI18n()
@@ -149,7 +147,7 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "change-tab", value: RESTOptionTabs): void (e: "change-tab", value: RequestOptionTabs): void
(e: "update:headers", value: HoppRESTHeader[]): void (e: "update:headers", value: HoppRESTHeader[]): void
(e: "update:body", value: HoppRESTReqBody): void (e: "update:body", value: HoppRESTReqBody): void
}>() }>()
@@ -166,7 +164,7 @@ const overridenContentType = computed(() =>
) )
) )
const contentTypeOverride = (tab: RESTOptionTabs) => { const contentTypeOverride = (tab: RequestOptionTabs) => {
emit("change-tab", tab) emit("change-tab", tab)
if (!isContentTypeAlreadyExist()) { if (!isContentTypeAlreadyExist()) {
// TODO: Fix this // TODO: Fix this

View File

@@ -157,10 +157,9 @@ import {
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconWrapText from "~icons/lucide/wrap-text" import IconWrapText from "~icons/lucide/wrap-text"
import { currentActiveTab } from "~/helpers/rest/tab"
import cloneDeep from "lodash-es/cloneDeep" import cloneDeep from "lodash-es/cloneDeep"
import { platform } from "~/platform" import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
@@ -174,8 +173,7 @@ const emit = defineEmits<{
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService) const request = ref(cloneDeep(currentActiveTab.value.document.request))
const request = ref(cloneDeep(tabs.currentActiveTab.value.document.request))
const codegenType = ref<CodegenName>("shell-curl") const codegenType = ref<CodegenName>("shell-curl")
const errorState = ref(false) const errorState = ref(false)
@@ -244,7 +242,7 @@ watch(
() => props.show, () => props.show,
(goingToShow) => { (goingToShow) => {
if (goingToShow) { if (goingToShow) {
request.value = cloneDeep(tabs.currentActiveTab.value.document.request) request.value = cloneDeep(currentActiveTab.value.document.request)
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_REST_CODEGEN_OPENED", type: "HOPP_REST_CODEGEN_OPENED",

View File

@@ -185,24 +185,18 @@
<span> <span>
<HoppButtonSecondary <HoppButtonSecondary
v-if="header.source === 'auth'" v-if="header.source === 'auth'"
v-tippy="{ theme: 'tooltip' }"
:title="t(masking ? 'state.show' : 'state.hide')"
:icon="masking ? IconEye : IconEyeOff" :icon="masking ? IconEye : IconEyeOff"
@click="toggleMask()" @click="toggleMask()"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
v-tippy="{ theme: 'tooltip' }"
:icon="IconArrowUpRight" :icon="IconArrowUpRight"
:title="t('request.go_to_authorization_tab')"
class="cursor-auto text-primary hover:text-primary" class="cursor-auto text-primary hover:text-primary"
/> />
</span> </span>
<span> <span>
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconArrowUpRight" :icon="IconArrowUpRight"
:title="t('request.go_to_authorization_tab')"
@click="changeTab(header.source)" @click="changeTab(header.source)"
/> />
</span> </span>
@@ -256,7 +250,7 @@ import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array" import * as A from "fp-ts/Array"
import draggable from "vuedraggable-es" import draggable from "vuedraggable-es"
import { RESTOptionTabs } from "./RequestOptions.vue" import { RequestOptionTabs } from "./RequestOptions.vue"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { commonHeaders } from "~/helpers/headers" import { commonHeaders } from "~/helpers/headers"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
@@ -273,13 +267,10 @@ import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection" import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest" import { currentTabID } from "~/helpers/rest/tab"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const colorMode = useColorMode() const colorMode = useColorMode()
const idTicker = ref(0) const idTicker = ref(0)
@@ -295,7 +286,7 @@ const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const props = defineProps<{ modelValue: HoppRESTRequest }>() const props = defineProps<{ modelValue: HoppRESTRequest }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "change-tab", value: RESTOptionTabs): void (e: "change-tab", value: RequestOptionTabs): void
(e: "update:modelValue", value: HoppRESTRequest): void (e: "update:modelValue", value: HoppRESTRequest): void
}>() }>()
@@ -518,13 +509,13 @@ const changeTab = (tab: ComputedHeader["source"]) => {
const inspectionService = useService(InspectionService) const inspectionService = useService(InspectionService)
const headerKeyResults = inspectionService.getResultViewFor( const headerKeyResults = inspectionService.getResultViewFor(
tabs.currentTabID.value, currentTabID.value,
(result) => (result) =>
result.locations.type === "header" && result.locations.position === "key" result.locations.type === "header" && result.locations.position === "key"
) )
const headerValueResults = inspectionService.getResultViewFor( const headerValueResults = inspectionService.getResultViewFor(
tabs.currentTabID.value, currentTabID.value,
(result) => (result) =>
result.locations.type === "header" && result.locations.position === "value" result.locations.type === "header" && result.locations.position === "value"
) )

View File

@@ -93,16 +93,13 @@ import IconWrapText from "~icons/lucide/wrap-text"
import IconClipboard from "~icons/lucide/clipboard" import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import { currentActiveTab } from "~/helpers/rest/tab"
import { platform } from "~/platform" import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const curl = ref("") const curl = ref("")
const curlEditor = ref<any | null>(null) const curlEditor = ref<any | null>(null)
@@ -152,7 +149,7 @@ const handleImport = () => {
type: "HOPP_REST_IMPORT_CURL", type: "HOPP_REST_IMPORT_CURL",
}) })
tabs.currentActiveTab.value.document.request = req currentActiveTab.value.document.request = req
} catch (e) { } catch (e) {
console.error(e) console.error(e)
toast.error(`${t("error.curl_invalid_format")}`) toast.error(`${t("error.curl_invalid_format")}`)

View File

@@ -202,13 +202,12 @@ import { objRemoveKey } from "@functional/object"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection" import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest" import { currentTabID } from "~/helpers/rest/tab"
const colorMode = useColorMode() const colorMode = useColorMode()
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const idTicker = ref(0) const idTicker = ref(0)
@@ -411,13 +410,13 @@ const clearContent = () => {
const inspectionService = useService(InspectionService) const inspectionService = useService(InspectionService)
const parameterKeyResults = inspectionService.getResultViewFor( const parameterKeyResults = inspectionService.getResultViewFor(
tabs.currentTabID.value, currentTabID.value,
(result) => (result) =>
result.locations.type === "parameter" && result.locations.position === "key" result.locations.type === "parameter" && result.locations.position === "key"
) )
const parameterValueResults = inspectionService.getResultViewFor( const parameterValueResults = inspectionService.getResultViewFor(
tabs.currentTabID.value, currentTabID.value,
(result) => (result) =>
result.locations.type === "parameter" && result.locations.type === "parameter" &&
result.locations.position === "value" result.locations.position === "value"

View File

@@ -217,7 +217,6 @@
@hide-modal="showCurlImportModal = false" @hide-modal="showCurlImportModal = false"
/> />
<HttpCodegenModal <HttpCodegenModal
v-if="showCodegenModal"
:show="showCodegenModal" :show="showCodegenModal"
@hide-modal="showCodegenModal = false" @hide-modal="showCodegenModal = false"
/> />
@@ -258,6 +257,7 @@ import IconLink2 from "~icons/lucide/link-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw" import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save" import IconSave from "~icons/lucide/save"
import IconShare2 from "~icons/lucide/share-2" import IconShare2 from "~icons/lucide/share-2"
import { HoppRESTTab, currentTabID } from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default" import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history" import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
import { platform } from "~/platform" import { platform } from "~/platform"
@@ -265,9 +265,6 @@ import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection" import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { RESTTabService } from "~/services/tab/rest"
const t = useI18n() const t = useI18n()
const interceptorService = useService(InterceptorService) const interceptorService = useService(InterceptorService)
@@ -289,7 +286,7 @@ const toast = useToast()
const { subscribeToStream } = useStreamSubscriber() const { subscribeToStream } = useStreamSubscriber()
const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>() const props = defineProps<{ modelValue: HoppRESTTab }>()
const emit = defineEmits(["update:modelValue"]) const emit = defineEmits(["update:modelValue"])
const tab = useVModel(props, "modelValue", emit) const tab = useVModel(props, "modelValue", emit)
@@ -429,7 +426,7 @@ const updateMethod = (method: string) => {
const onSelectMethod = (e: Event | any) => { const onSelectMethod = (e: Event | any) => {
// type any because of value property not being recognized by TS in the event.target object. It is a valid property though. // type any because of value property not being recognized by TS in the event.target object. It is a valid property though.
updateMethod(e.target.value) updateMethod(e.value)
} }
const clearContent = () => { const clearContent = () => {
@@ -437,7 +434,7 @@ const clearContent = () => {
} }
const updateRESTResponse = (response: HoppRESTResponse | null) => { const updateRESTResponse = (response: HoppRESTResponse | null) => {
tab.value.document.response = response tab.value.response = response
} }
const copyLinkIcon = refAutoReset< const copyLinkIcon = refAutoReset<
@@ -645,6 +642,5 @@ const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService) const inspectionService = useService(InspectionService)
const tabs = useService(RESTTabService) const tabResults = inspectionService.getResultViewFor(currentTabID.value)
const tabResults = inspectionService.getResultViewFor(tabs.currentTabID.value)
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<HoppSmartTabs <HoppSmartTabs
v-model="selectedOptionTab" v-model="selectedOptionsTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10" styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
render-inactive-tabs render-inactive-tabs
> >
@@ -15,7 +15,7 @@
<HttpBody <HttpBody
v-model:headers="request.headers" v-model:headers="request.headers"
v-model:body="request.body" v-model:body="request.body"
@change-tab="changeOptionTab" @change-tab="changeTab"
/> />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
@@ -23,7 +23,7 @@
:label="`${t('tab.headers')}`" :label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`" :info="`${newActiveHeadersCount$}`"
> >
<HttpHeaders v-model="request" @change-tab="changeOptionTab" /> <HttpHeaders v-model="request" @change-tab="changeTab" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`"> <HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<HttpAuthorization v-model="request.auth" /> <HttpAuthorization v-model="request.auth" />
@@ -55,43 +55,31 @@
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { HoppRESTRequest } from "@hoppscotch/data" import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { computed } from "vue" import { computed, ref } from "vue"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
const VALID_OPTION_TABS = [ export type RequestOptionTabs =
"params", | "params"
"bodyParams", | "bodyParams"
"headers", | "headers"
"authorization", | "authorization"
"preRequestScript", | "preRequestScript"
"tests", | "tests"
] as const
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
const t = useI18n() const t = useI18n()
// v-model integration with props and emit // v-model integration with props and emit
const props = withDefaults( const props = defineProps<{ modelValue: HoppRESTRequest }>()
defineProps<{
modelValue: HoppRESTRequest
optionTab: RESTOptionTabs
}>(),
{
optionTab: "params",
}
)
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTRequest): void (e: "update:modelValue", value: HoppRESTRequest): void
(e: "update:optionTab", value: RESTOptionTabs): void
}>() }>()
const request = useVModel(props, "modelValue", emit) const request = useVModel(props, "modelValue", emit)
const selectedOptionTab = useVModel(props, "optionTab", emit)
const changeOptionTab = (e: RESTOptionTabs) => { const selectedOptionsTab = ref<RequestOptionTabs>("params")
selectedOptionTab.value = e
const changeTab = (e: RequestOptionTabs) => {
selectedOptionsTab.value = e
} }
const newActiveParamsCount$ = computed(() => { const newActiveParamsCount$ = computed(() => {
@@ -113,6 +101,6 @@ const newActiveHeadersCount$ = computed(() => {
}) })
defineActionHandler("request.open-tab", ({ tab }) => { defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionTab.value = tab as RESTOptionTabs selectedOptionsTab.value = tab as RequestOptionTabs
}) })
</script> </script>

View File

@@ -2,13 +2,10 @@
<AppPaneLayout layout-id="rest-primary"> <AppPaneLayout layout-id="rest-primary">
<template #primary> <template #primary>
<HttpRequest v-model="tab" /> <HttpRequest v-model="tab" />
<HttpRequestOptions <HttpRequestOptions v-model="tab.document.request" />
v-model="tab.document.request"
v-model:option-tab="tab.document.optionTabPreference"
/>
</template> </template>
<template #secondary> <template #secondary>
<HttpResponse v-model:document="tab.document" /> <HttpResponse v-model:tab="tab" />
</template> </template>
</AppPaneLayout> </AppPaneLayout>
</template> </template>
@@ -16,17 +13,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { watch } from "vue" import { watch } from "vue"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { isEqualHoppRESTRequest } from "@hoppscotch/data" import { isEqualHoppRESTRequest } from "@hoppscotch/data"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
// TODO: Move Response and Request execution code to over here // TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>() const props = defineProps<{ modelValue: HoppRESTTab }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", val: HoppTab<HoppRESTDocument>): void (e: "update:modelValue", val: HoppRESTTab): void
}>() }>()
const tab = useVModel(props, "modelValue", emit) const tab = useVModel(props, "modelValue", emit)

View File

@@ -1,33 +1,36 @@
<template> <template>
<div class="flex flex-col flex-1 relative"> <div class="flex flex-col flex-1 relative">
<HttpResponseMeta :response="doc.response" /> <HttpResponseMeta :response="tab.response" />
<LensesResponseBodyRenderer <LensesResponseBodyRenderer
v-if="!loading && hasResponse" v-if="!loading && hasResponse"
v-model:document="doc" v-model:selected-tab-preference="selectedTabPreference"
v-model:tab="tab"
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { computed } from "vue"
import { HoppRESTDocument } from "~/helpers/rest/document"
const props = defineProps<{ const props = defineProps<{
document: HoppRESTDocument tab: HoppRESTTab
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:tab", val: HoppRESTDocument): void (e: "update:tab", val: HoppRESTTab): void
}>() }>()
const doc = useVModel(props, "document", emit) const tab = useVModel(props, "tab", emit)
const selectedTabPreference = ref<string | null>(null)
const hasResponse = computed( const hasResponse = computed(
() => () =>
doc.value.response?.type === "success" || tab.value.response?.type === "success" ||
doc.value.response?.type === "fail" tab.value.response?.type === "fail"
) )
const loading = computed(() => doc.value.response?.type === "loading") const loading = computed(() => tab.value.response?.type === "loading")
</script> </script>

View File

@@ -93,11 +93,10 @@ import { useColorMode } from "@composables/theming"
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes" import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection" import { InspectionService } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest" import { currentTabID } from "~/helpers/rest/tab"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const tabs = useService(RESTTabService)
const props = defineProps<{ const props = defineProps<{
response: HoppRESTResponse | null | undefined response: HoppRESTResponse | null | undefined
@@ -147,7 +146,7 @@ const statusCategory = computed(() => {
const inspectionService = useService(InspectionService) const inspectionService = useService(InspectionService)
const tabResults = inspectionService.getResultViewFor( const tabResults = inspectionService.getResultViewFor(
tabs.currentTabID.value, currentTabID.value,
(result) => result.locations.type === "response" (result) => result.locations.type === "response"
) )
</script> </script>

View File

@@ -96,17 +96,16 @@ import { ref } from "vue"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring" import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { HoppRESTTab } from "~/helpers/rest/tab"
import IconXCircle from "~icons/lucide/x-circle" import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square" import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit" import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
const t = useI18n() const t = useI18n()
defineProps<{ defineProps<{
tab: HoppTab<HoppRESTDocument> tab: HoppRESTTab
isRemovable: boolean isRemovable: boolean
}>() }>()

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