Compare commits

..

15 Commits

Author SHA1 Message Date
Balu Babu
9eb067feaf chore: fixed issues with pnpm-lock file 2023-08-03 19:39:35 +05:30
Balu Babu
0b5f57436f chore: modified error code for magic-link provider check 2023-08-03 19:36:41 +05:30
Mir Arif Hasan
0c0ed5610e chore: env check func moved to utils file 2023-08-03 19:36:32 +05:30
Mir Arif Hasan
dce032a275 feat: feedback applied 2023-08-03 19:36:17 +05:30
Mir Arif Hasan
2599b1d326 chore: check added if ALLOWED_AUTH_PROVIDERS is there in the env file or not 2023-08-03 19:35:40 +05:30
Mir Arif Hasan
419e376f46 fix: provider return type in SSO guards 2023-08-03 19:35:21 +05:30
Mir Arif Hasan
c79fcbeceb chore: handled internal server error for missing auth providers 2023-08-03 19:35:14 +05:30
Mir Arif Hasan
092cb4c3a5 chore: auth provider name read from enum 2023-08-03 19:35:06 +05:30
Mir Arif Hasan
d3f25361f7 chore: removed unused imports 2023-08-03 19:34:57 +05:30
Mir Arif Hasan
09c13e86b2 feat: remove EmptyClassProvider class 2023-08-03 19:34:48 +05:30
Balu Babu
04bb219c12 chore: fixed mistake in AUTH_PROVIDER_NOT_SPECIFIED error description 2023-08-03 19:34:40 +05:30
Balu Babu
ca79cf40b1 chore: changed target of hoppscotch-backend service back to prod in docker.compose file 2023-08-03 19:34:32 +05:30
Balu Babu
454c82975e chore: added comments to authProviderCheck function in auth/helper.ts 2023-08-03 19:34:24 +05:30
Balu Babu
c38488dfc4 feat: magic-link can now be conditionally provisioned 2023-08-03 19:34:16 +05:30
Balu Babu
2d0ebedbbb feat: social auth providers can now be conditionally provisioned 2023-08-03 19:34:07 +05:30
260 changed files with 9542 additions and 17976 deletions

View File

@@ -1,2 +0,0 @@
node_modules
**/*/node_modules

View File

@@ -13,7 +13,7 @@ SESSION_SECRET='add some secret here'
# Hoppscotch App Domain Config # Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000" REDIRECT_URL="http://localhost:3000"
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100" WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
# Google Auth Config # Google Auth Config
GOOGLE_CLIENT_ID="************************************************" GOOGLE_CLIENT_ID="************************************************"

View File

@@ -1,66 +0,0 @@
name: "Push containers to Docker Hub on release"
on:
push:
tags:
- '*.*.*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
run: cp .env.example .env
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push `${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: backend
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: app
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: sh_admin
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_AIO_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: aio
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:${{ github.ref_name }}

View File

@@ -2,9 +2,9 @@ name: Node.js CI
on: on:
push: push:
branches: [main, staging, "release/**"] branches: [main, staging]
pull_request: pull_request:
branches: [main, staging, "release/**"] branches: [main, staging]
jobs: jobs:
test: test:

View File

@@ -1,8 +1,3 @@
module.exports = { module.exports = {
semi: false, semi: false
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
} }

View File

@@ -1,11 +0,0 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin
file_server
}

View File

@@ -1,72 +0,0 @@
#!/usr/local/bin/node
// @ts-check
import { execSync, spawn } from "child_process"
import fs from "fs"
import process from "process"
function runChildProcessWithPrefix(command, args, prefix) {
const childProcess = spawn(command, args);
childProcess.stdout.on('data', (data) => {
const output = data.toString().trim().split('\n');
output.forEach((line) => {
console.log(`${prefix} | ${line}`);
});
});
childProcess.stderr.on('data', (data) => {
const error = data.toString().trim().split('\n');
error.forEach((line) => {
console.error(`${prefix} | ${line}`);
});
});
childProcess.on('close', (code) => {
console.log(`${prefix} Child process exited with code ${code}`);
});
childProcess.on('error', (stuff) => {
console.log("error")
console.log(stuff)
})
return childProcess
}
const envFileContent = Object.entries(process.env)
.filter(([env]) => env.startsWith("VITE_"))
.map(([env, val]) => `${env}=${
(val.startsWith("\"") && val.endsWith("\""))
? val
: `"${val}"`
}`)
.join("\n")
fs.writeFileSync("build.env", envFileContent)
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
fs.rmSync("build.env")
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
caddyProcess.on("exit", (code) => {
console.log(`Exiting process because Caddy Server exited with code ${code}`)
process.exit(code)
})
backendProcess.on("exit", (code) => {
console.log(`Exiting process because Backend Server exited with code ${code}`)
process.exit(code)
})
process.on('SIGINT', () => {
console.log("SIGINT received, exiting...")
caddyProcess.kill("SIGINT")
backendProcess.kill("SIGINT")
process.exit(0)
})

View File

@@ -8,25 +8,23 @@ services:
hoppscotch-backend: hoppscotch-backend:
container_name: hoppscotch-backend container_name: hoppscotch-backend
build: build:
dockerfile: prod.Dockerfile dockerfile: packages/hoppscotch-backend/Dockerfile
context: . context: .
target: backend target: prod
env_file: env_file:
- ./.env - ./.env
restart: always restart: always
environment: environment:
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well) # Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300 - DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
- PORT=3170 - PORT=3000
volumes: volumes:
# Uncomment the line below when modifying code. Only applicable when using the "dev" target. - ./packages/hoppscotch-backend/:/usr/src/app
# - ./packages/hoppscotch-backend/:/usr/src/app
- /usr/src/app/node_modules/ - /usr/src/app/node_modules/
depends_on: depends_on:
hoppscotch-db: - hoppscotch-db
condition: service_healthy
ports: ports:
- "3170:3170" - "3170:3000"
# The main hoppscotch app. This will be hosted at port 3000 # The main hoppscotch app. This will be hosted at port 3000
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for # NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
@@ -34,9 +32,8 @@ services:
hoppscotch-app: hoppscotch-app:
container_name: hoppscotch-app container_name: hoppscotch-app
build: build:
dockerfile: prod.Dockerfile dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
context: . context: .
target: app
env_file: env_file:
- ./.env - ./.env
depends_on: depends_on:
@@ -50,9 +47,8 @@ services:
hoppscotch-sh-admin: hoppscotch-sh-admin:
container_name: hoppscotch-sh-admin container_name: hoppscotch-sh-admin
build: build:
dockerfile: prod.Dockerfile dockerfile: packages/hoppscotch-sh-admin/Dockerfile
context: . context: .
target: sh_admin
env_file: env_file:
- ./.env - ./.env
depends_on: depends_on:
@@ -60,91 +56,16 @@ services:
ports: ports:
- "3100:8080" - "3100:8080"
# The service that spins up all 3 services at once in one container
hoppscotch-aio:
container_name: hoppscotch-aio
build:
dockerfile: prod.Dockerfile
context: .
target: aio
env_file:
- ./.env
depends_on:
hoppscotch-db:
condition: service_healthy
ports:
- "3000:3000"
- "3100:3100"
- "3170:3170"
# The preset DB service, you can delete/comment the below lines if # The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance # you are using an external postgres instance
# This will be exposed at port 5432 # This will be exposed at port 5432
hoppscotch-db: hoppscotch-db:
image: postgres:15 image: postgres
ports: ports:
- "5432:5432" - "5432:5432"
user: postgres
environment: environment:
# The default user defined by the docker image
POSTGRES_USER: postgres
# NOTE: Please UPDATE THIS PASSWORD! # NOTE: Please UPDATE THIS PASSWORD!
POSTGRES_PASSWORD: testpass POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch POSTGRES_DB: hoppscotch
healthcheck:
test:
[
"CMD-SHELL",
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
]
interval: 5s
timeout: 5s
retries: 10
# All the services listed below are deprececated
hoppscotch-old-backend:
container_name: hoppscotch-old-backend
build:
dockerfile: packages/hoppscotch-backend/Dockerfile
context: .
target: prod
env_file:
- ./.env
restart: always
environment:
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
- PORT=3000
volumes:
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
# - ./packages/hoppscotch-backend/:/usr/src/app
- /usr/src/app/node_modules/
depends_on:
hoppscotch-db:
condition: service_healthy
ports:
- "3170:3000"
hoppscotch-old-app:
container_name: hoppscotch-old-app
build:
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
context: .
env_file:
- ./.env
depends_on:
- hoppscotch-old-backend
ports:
- "3000:8080"
hoppscotch-old-sh-admin:
container_name: hoppscotch-old-sh-admin
build:
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
context: .
env_file:
- ./.env
depends_on:
- hoppscotch-old-backend
ports:
- "3100:8080"

View File

@@ -1,14 +0,0 @@
#!/bin/bash
curlCheck() {
if ! curl -s --head "$1" | head -n 1 | grep -q "HTTP/1.[01] [23].."; then
echo "URL request failed!"
exit 1
else
echo "URL request succeeded!"
fi
}
curlCheck "http://localhost:3000"
curlCheck "http://localhost:3100"
curlCheck "http://localhost:3170/ping"

View File

@@ -32,14 +32,5 @@
"@types/node": "^17.0.24", "@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"
},
"pnpm": {
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {
"ajv": "6.12.3"
}
}
}
} }
} }

View File

@@ -17,12 +17,12 @@
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@codemirror/language": "^6.9.0", "@codemirror/language": "^6.2.0",
"@lezer/highlight": "^1.1.6", "@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.10" "@lezer/lr": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.5.0", "@lezer/generator": "^1.1.0",
"mocha": "^9.2.2", "mocha": "^9.2.2",
"rollup": "^2.70.2", "rollup": "^2.70.2",
"rollup-plugin-dts": "^4.2.1", "rollup-plugin-dts": "^4.2.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoppscotch-backend", "name": "hoppscotch-backend",
"version": "2023.8.0", "version": "2023.4.7",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -33,7 +33,7 @@
"@nestjs/passport": "^9.0.0", "@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.2.1", "@nestjs/platform-express": "^9.2.1",
"@nestjs/throttler": "^4.0.0", "@nestjs/throttler": "^4.0.0",
"@prisma/client": "^4.16.2", "@prisma/client": "^4.7.1",
"apollo-server-express": "^3.11.1", "apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1", "apollo-server-plugin-base": "^3.7.1",
"argon2": "^0.30.3", "argon2": "^0.30.3",
@@ -57,7 +57,7 @@
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"passport-microsoft": "^1.0.0", "passport-microsoft": "^1.0.0",
"prisma": "^4.16.2", "prisma": "^4.7.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.6.0" "rxjs": "^7.6.0"

View File

@@ -5,7 +5,7 @@ datasource db {
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] binaryTargets = ["native", "debian-openssl-1.1.x"]
} }
model Team { model Team {

View File

@@ -1,9 +0,0 @@
import { Controller, Get } from '@nestjs/common';
@Controller('ping')
export class AppController {
@Get()
ping(): string {
return 'Success';
}
}

View File

@@ -19,7 +19,6 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
import { ShortcodeModule } from './shortcode/shortcode.module'; import { ShortcodeModule } from './shortcode/shortcode.module';
import { COOKIES_NOT_FOUND } from './errors'; import { COOKIES_NOT_FOUND } from './errors';
import { ThrottlerModule } from '@nestjs/throttler'; import { ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app.controller';
@Module({ @Module({
imports: [ imports: [
@@ -82,6 +81,5 @@ import { AppController } from './app.controller';
ShortcodeModule, ShortcodeModule,
], ],
providers: [GQLComplexityPlugin], providers: [GQLComplexityPlugin],
controllers: [AppController],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -107,7 +107,7 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
}; };
/** /**
* Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable * Check to see if given auth provider is present in the ALLOWED_AUTH_PROVIDERS env variable
* *
* @param provider Provider we want to check the presence of * @param provider Provider we want to check the presence of
* @returns Boolean if provider specified is present or not * @returns Boolean if provider specified is present or not
@@ -117,8 +117,8 @@ export function authProviderCheck(provider: string) {
throwErr(AUTH_PROVIDER_NOT_SPECIFIED); throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
} }
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS const envVariables = process.env.ALLOWED_AUTH_PROVIDERS
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) => ? process.env.ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
provider.trim().toUpperCase(), provider.trim().toUpperCase(),
) )
: []; : [];

View File

@@ -29,22 +29,22 @@ export const JSON_INVALID = 'json_invalid';
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified'; export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
/** /**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file * Environment variable "ALLOWED_AUTH_PROVIDERS" is not present in .env file
*/ */
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS = export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file'; '"ALLOWED_AUTH_PROVIDERS" is not present in .env file';
/** /**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file * Environment variable "ALLOWED_AUTH_PROVIDERS" is empty in .env file
*/ */
export const ENV_EMPTY_AUTH_PROVIDERS = export const ENV_EMPTY_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file'; '"ALLOWED_AUTH_PROVIDERS" is empty in .env file';
/** /**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file * Environment variable "ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
*/ */
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS = export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
'"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file'; '"ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
/** /**
* Tried to delete a user data document from fb firestore but failed. * Tried to delete a user data document from fb firestore but failed.

View File

@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
orderBy: { orderBy: {
createdOn: 'desc', createdOn: 'desc',
}, },
skip: args.cursor ? 1 : 0, skip: 1,
take: args.take, take: args.take,
cursor: args.cursor ? { id: args.cursor } : undefined, cursor: args.cursor ? { id: args.cursor } : undefined,
}); });

View File

@@ -306,8 +306,8 @@ describe('TeamEnvironmentsService', () => {
); );
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({ mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
id: 'newid',
...teamEnvironment, ...teamEnvironment,
id: 'newid',
}); });
const result = await teamEnvironmentsService.createDuplicateEnvironment( const result = await teamEnvironmentsService.createDuplicateEnvironment(
@@ -337,8 +337,8 @@ describe('TeamEnvironmentsService', () => {
); );
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({ mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
id: 'newid',
...teamEnvironment, ...teamEnvironment,
id: 'newid',
}); });
const result = await teamEnvironmentsService.createDuplicateEnvironment( const result = await teamEnvironmentsService.createDuplicateEnvironment(

View File

@@ -24,8 +24,6 @@ beforeEach(() => {
mockPubSub.publish.mockClear(); mockPubSub.publish.mockClear();
}); });
const date = new Date();
describe('UserHistoryService', () => { describe('UserHistoryService', () => {
describe('fetchUserHistory', () => { describe('fetchUserHistory', () => {
test('Should return a list of users REST history if exists', async () => { test('Should return a list of users REST history if exists', async () => {
@@ -402,7 +400,7 @@ describe('UserHistoryService', () => {
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: date, executedOn: new Date(),
isStarred: false, isStarred: false,
}); });
@@ -412,7 +410,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: date, executedOn: new Date(),
isStarred: false, isStarred: false,
}; };

View File

@@ -9,12 +9,7 @@ import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array'; import * as A from 'fp-ts/Array';
import { TeamMemberRole } from './team/team.model'; import { TeamMemberRole } from './team/team.model';
import { User } from './user/user.model'; import { User } from './user/user.model';
import { import { ENV_EMPTY_AUTH_PROVIDERS, ENV_NOT_FOUND_KEY_AUTH_PROVIDERS, ENV_NOT_SUPPORT_AUTH_PROVIDERS, JSON_INVALID } from './errors';
ENV_EMPTY_AUTH_PROVIDERS,
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
JSON_INVALID,
} from './errors';
import { AuthProvider } from './auth/helper'; import { AuthProvider } from './auth/helper';
/** /**
@@ -161,21 +156,21 @@ export function isValidLength(title: string, length: number) {
/** /**
* This function is called by bootstrap() in main.ts * This function is called by bootstrap() in main.ts
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not. * It checks if the "ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* If not, it throws an error. * If not, it throws an error.
*/ */
export function checkEnvironmentAuthProvider() { export function checkEnvironmentAuthProvider() {
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) { if (!process.env.hasOwnProperty('ALLOWED_AUTH_PROVIDERS')) {
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS); throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
} }
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') { if (process.env.ALLOWED_AUTH_PROVIDERS === '') {
throw new Error(ENV_EMPTY_AUTH_PROVIDERS); throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
} }
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split( const givenAuthProviders = process.env.ALLOWED_AUTH_PROVIDERS.split(',').map(
',', (provider) => provider.toLocaleUpperCase(),
).map((provider) => provider.toLocaleUpperCase()); );
const supportedAuthProviders = Object.values(AuthProvider).map( const supportedAuthProviders = Object.values(AuthProvider).map(
(provider: string) => provider.toLocaleUpperCase(), (provider: string) => provider.toLocaleUpperCase(),
); );

View File

@@ -29,18 +29,8 @@ module.exports = {
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154 "import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": "off", "no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn", "no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier": [ "prettier/prettier":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn", process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{},
{
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2,
},
],
"vue/multi-word-component-names": "off", "vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off", "vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off", "import/no-named-as-default": "off",

View File

@@ -1,8 +1,3 @@
module.exports = { module.exports = {
semi: false, semi: false
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
} }

View File

@@ -166,6 +166,12 @@ a {
@apply truncate; @apply truncate;
@apply sm:inline-flex; @apply sm:inline-flex;
} }
.env-icon {
@apply transition;
@apply inline-flex;
@apply items-center;
}
} }
.tippy-svg-arrow { .tippy-svg-arrow {
@@ -184,11 +190,10 @@ a {
@apply border-solid border-dividerDark; @apply border-solid border-dividerDark;
@apply rounded; @apply rounded;
@apply shadow-lg; @apply shadow-lg;
@apply max-w-[45vw] #{!important};
.tippy-content { .tippy-content {
@apply flex flex-col; @apply flex flex-col;
@apply max-h-[45vh]; @apply max-h-56;
@apply items-stretch; @apply items-stretch;
@apply overflow-y-auto; @apply overflow-y-auto;
@apply text-secondary text-body; @apply text-secondary text-body;
@@ -196,10 +201,6 @@ a {
@apply leading-normal; @apply leading-normal;
@apply focus:outline-none; @apply focus:outline-none;
scroll-behavior: smooth; scroll-behavior: smooth;
& > span {
@apply block #{!important};
}
} }
.tippy-svg-arrow { .tippy-svg-arrow {
@@ -215,7 +216,6 @@ a {
[data-v-tippy] { [data-v-tippy] {
@apply flex flex-1; @apply flex flex-1;
@apply truncate;
} }
[interactive] > div { [interactive] > div {
@@ -326,7 +326,7 @@ pre.ace_editor {
@apply after:font-icon; @apply after:font-icon;
@apply after:text-current; @apply after:text-current;
@apply after:right-3; @apply after:right-3;
@apply after:content-["\e5cf"]; @apply after:content-["\e313"];
@apply after:text-lg; @apply after:text-lg;
} }
@@ -481,10 +481,6 @@ pre.ace_editor {
} }
} }
.cm-scroller {
@apply overscroll-y-auto;
}
.cm-editor { .cm-editor {
.cm-line::selection { .cm-line::selection {
@apply bg-accentDark #{!important}; @apply bg-accentDark #{!important};
@@ -572,11 +568,3 @@ details[open] summary .indicator {
@apply rounded; @apply rounded;
@apply border-0; @apply border-0;
} }
.gql-operation-not-highlight {
@apply opacity-50;
}
.gql-operation-highlight {
@apply opacity-100;
}

View File

@@ -1,7 +1,7 @@
@mixin base-theme { @mixin base-theme {
--font-sans: "Inter Variable", sans-serif; --font-sans: "Inter", sans-serif;
--font-icon: "Material Symbols Rounded Variable"; --font-mono: "Roboto Mono", monospace;
--font-mono: "Roboto Mono Variable", monospace; --font-icon: "Material Icons";
--font-size-tiny: calc(var(--font-size-body) - 0.062rem); --font-size-tiny: calc(var(--font-size-body) - 0.062rem);
} }

View File

@@ -31,7 +31,6 @@
"open_workspace": "Open workspace", "open_workspace": "Open workspace",
"paste": "Paste", "paste": "Paste",
"prettify": "Prettify", "prettify": "Prettify",
"rename": "Rename",
"remove": "Remove", "remove": "Remove",
"restore": "Restore", "restore": "Restore",
"save": "Save", "save": "Save",
@@ -69,8 +68,6 @@
"invite": "Invite", "invite": "Invite",
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.", "invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
"invite_your_friends": "Invite your friends", "invite_your_friends": "Invite your friends",
"social_links": "Social links",
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
"join_discord_community": "Join our Discord community", "join_discord_community": "Join our Discord community",
"keyboard_shortcuts": "Keyboard shortcuts", "keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch", "name": "Hoppscotch",
@@ -135,7 +132,6 @@
"renamed": "Collection renamed", "renamed": "Collection renamed",
"request_in_use": "Request in use", "request_in_use": "Request in use",
"save_as": "Save as", "save_as": "Save as",
"save_to_collection": "Save to Collection",
"select": "Select a Collection", "select": "Select a Collection",
"select_location": "Select location", "select_location": "Select location",
"select_team": "Select a team", "select_team": "Select a team",
@@ -153,14 +149,12 @@
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?", "remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.", "request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?", "save_unsaved_tab": "Do you want to save changes made in this tab?",
"close_unsaved_tab": "Are you sure you want to close this tab?",
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress." "sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
}, },
"context_menu": { "context_menu": {
"set_environment_variable": "Set as variable", "set_environment_variable": "Set as variable",
"add_parameters": "Add to parameters", "add_parameter": "Add to parameter",
"open_request_in_new_tab": "Open request in new tab" "open_link_in_new_tab": "Open link in new tab"
}, },
"count": { "count": {
"header": "Header {count}", "header": "Header {count}",
@@ -185,6 +179,7 @@
"folder": "Folder is empty", "folder": "Folder is empty",
"headers": "This request does not have any headers", "headers": "This request does not have any headers",
"history": "History is empty", "history": "History is empty",
"history_suggestions": "History does not have any matching entries",
"invites": "Invite list is empty", "invites": "Invite list is empty",
"members": "Team is empty", "members": "Team is empty",
"parameters": "This request does not have any parameters", "parameters": "This request does not have any parameters",
@@ -204,25 +199,18 @@
"create_new": "Create new environment", "create_new": "Create new environment",
"created": "Environment created", "created": "Environment created",
"deleted": "Environment deletion", "deleted": "Environment deletion",
"duplicated": "Environment duplicated",
"edit": "Edit Environment", "edit": "Edit Environment",
"global": "Global", "global": "Global",
"empty_variables": "No variables",
"global_variables": "Global variables",
"invalid_name": "Please provide a name for the environment", "invalid_name": "Please provide a name for the environment",
"list": "Environment variables",
"my_environments": "My Environments", "my_environments": "My Environments",
"name": "Name", "name": "Name",
"nested_overflow": "nested environment variables are limited to 10 levels", "nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment", "new": "New Environment",
"no_active_environment": "No active environment",
"no_environment": "No environment", "no_environment": "No environment",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.", "no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable", "replace_with_variable": "Replace with variable",
"scope": "Scope", "scope": "Scope",
"select": "Select environment", "select": "Select environment",
"set": "Set environment",
"set_as_environment": "Set as environment", "set_as_environment": "Set as environment",
"team_environments": "Team Environments", "team_environments": "Team Environments",
"title": "Environments", "title": "Environments",
@@ -252,7 +240,6 @@
"no_duration": "No duration", "no_duration": "No duration",
"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",
"script_fail": "Could not execute pre-request script", "script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong", "something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script" "test_script_fail": "Could not execute post-request script"
@@ -280,10 +267,6 @@
"graphql": { "graphql": {
"mutations": "Mutations", "mutations": "Mutations",
"schema": "Schema", "schema": "Schema",
"switch_connection": "Switch connection",
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
"subscriptions": "Subscriptions" "subscriptions": "Subscriptions"
}, },
"group": { "group": {
@@ -313,30 +296,6 @@
"preview": "Hide Preview", "preview": "Hide Preview",
"sidebar": "Collapse sidebar" "sidebar": "Collapse sidebar"
}, },
"inspections": {
"title": "Inspector",
"description": "Inspect possible errors",
"environment": {
"add_environment": "Add to Environment",
"not_found": "Environment variable “{environment}” not found."
},
"header": {
"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": {
"401_error": "Please check your authentication credentials.",
"404_error": "Please check your request URL and method type.",
"network_error": "Please check your network connection.",
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
"default_error": "Please check your request."
},
"url": {
"extension_not_installed": "Extension not installed.",
"extention_not_enabled": "Extension not enabled.",
"extention_enable_action": "Enable Browser Extension",
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list."
}
},
"import": { "import": {
"collections": "Import collections", "collections": "Import collections",
"curl": "Import cURL", "curl": "Import cURL",
@@ -473,10 +432,8 @@
"payload": "Payload", "payload": "Payload",
"query": "Query", "query": "Query",
"raw_body": "Raw Request Body", "raw_body": "Raw Request Body",
"rename": "Rename Request",
"renamed": "Request renamed", "renamed": "Request renamed",
"run": "Run", "run": "Run",
"stop": "Stop",
"save": "Save", "save": "Save",
"save_as": "Save as", "save_as": "Save as",
"saved": "Request saved", "saved": "Request saved",
@@ -516,9 +473,9 @@
"account_name_description": "This is your display name.", "account_name_description": "This is your display name.",
"background": "Background", "background": "Background",
"black_mode": "Black", "black_mode": "Black",
"dark_mode": "Dark",
"change_font_size": "Change font size", "change_font_size": "Change font size",
"choose_language": "Choose language", "choose_language": "Choose language",
"dark_mode": "Dark",
"delete_account": "Delete account", "delete_account": "Delete account",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.", "delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"expand_navigation": "Expand navigation", "expand_navigation": "Expand navigation",
@@ -582,10 +539,6 @@
"show_all": "Keyboard shortcuts", "show_all": "Keyboard shortcuts",
"title": "General" "title": "General"
}, },
"others": {
"title": "Others",
"prettify": "Prettify Editor's Content"
},
"miscellaneous": { "miscellaneous": {
"invite": "Invite people to Hoppscotch", "invite": "Invite people to Hoppscotch",
"title": "Miscellaneous" "title": "Miscellaneous"
@@ -606,9 +559,6 @@
"delete_method": "Select DELETE method", "delete_method": "Select DELETE method",
"get_method": "Select GET method", "get_method": "Select GET method",
"head_method": "Select HEAD method", "head_method": "Select HEAD method",
"rename": "Rename Request",
"import_curl": "Import cURL",
"show_code": "Generate code snippet",
"method": "Method", "method": "Method",
"next_method": "Select Next method", "next_method": "Select Next method",
"post_method": "Select POST method", "post_method": "Select POST method",
@@ -617,7 +567,6 @@
"reset_request": "Reset Request", "reset_request": "Reset Request",
"save_to_collections": "Save to Collections", "save_to_collections": "Save to Collections",
"send_request": "Send Request", "send_request": "Send Request",
"save_request": "Save Request",
"title": "Request" "title": "Request"
}, },
"response": { "response": {
@@ -626,10 +575,10 @@
"title": "Response" "title": "Response"
}, },
"theme": { "theme": {
"black": "Switch theme to Black Mode", "black": "Switch theme to black mode",
"dark": "Switch theme to Dark Mode", "dark": "Switch theme to dark mode",
"light": "Switch theme to Light Mode", "light": "Switch theme to light mode",
"system": "Switch theme to System Mode", "system": "Switch theme to system mode",
"title": "Theme" "title": "Theme"
} }
}, },
@@ -648,87 +597,8 @@
"url": "URL" "url": "URL"
}, },
"spotlight": { "spotlight": {
"general": {
"help_menu": "Help and support",
"chat": "Chat with support",
"open_docs": "Read Documentation",
"open_keybindings": "Keyboard shortcuts",
"open_github": "Open GitHub repository",
"social": "Social",
"title": "General"
},
"miscellaneous": {
"invite": "Invite your friends to Hoppscotch",
"title": "Miscellaneous"
},
"request": {
"switch_to": "Switch to",
"select_method": "Select method",
"save_as_new": "Save as new request",
"tab_parameters": "Parameters tab",
"tab_body": "Body tab",
"tab_headers": "Headers tab",
"tab_authorization": "Authorization tab",
"tab_pre_request_script": "Pre-request script tab",
"tab_tests": "Tests tab",
"tab_query": "Query tab",
"tab_variables": "Variables tab"
},
"graphql": {
"connect": "Connect to server",
"disconnect": "Disconnect from server"
},
"response": {
"copy": "Copy response",
"download": "Download response as file",
"title": "Response"
},
"environments": {
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"edit": "Edit current environment",
"delete": "Delete current environment",
"duplicate": "Duplicate current environment",
"edit_global": "Edit global environment",
"duplicate_global": "Duplicate global environment",
"title": "Environments"
},
"workspace": {
"new": "Create new team",
"edit": "Edit current team",
"delete": "Delete current team",
"invite": "Invite people to team",
"switch_to_personal": "Switch to your personal workspace",
"title": "Teams"
},
"tab": {
"duplicate": "Duplicate current tab",
"close_current": "Close current tab",
"close_others": "Close all other tabs",
"new_tab": "Open a new tab",
"title": "Tabs"
},
"section": { "section": {
"user": "User", "user": "User"
"theme": "Theme",
"interface": "Interface",
"interceptor": "Interceptor"
},
"change_language": "Change Language",
"settings": {
"theme": {
"black": "Black",
"dark": "Dark",
"light": "Light",
"system": "System preference"
},
"font": {
"size_sm": "Small",
"size_md": "Medium",
"size_lg": "Large"
},
"change_interceptor": "Change Interceptor",
"change_language": "Change Language"
} }
}, },
"sse": { "sse": {
@@ -788,11 +658,8 @@
"tab": { "tab": {
"authorization": "Authorization", "authorization": "Authorization",
"body": "Body", "body": "Body",
"close": "Close Tab",
"close_others": "Close other Tabs",
"collections": "Collections", "collections": "Collections",
"documentation": "Documentation", "documentation": "Documentation",
"duplicate": "Duplicate Tab",
"environments": "Environments", "environments": "Environments",
"headers": "Headers", "headers": "Headers",
"history": "History", "history": "History",

View File

@@ -19,7 +19,7 @@
"edit": "編輯", "edit": "編輯",
"filter": "篩選回應", "filter": "篩選回應",
"go_back": "返回", "go_back": "返回",
"go_forward": "向前", "go_forward": "Go forward",
"group_by": "分組方式", "group_by": "分組方式",
"label": "標籤", "label": "標籤",
"learn_more": "瞭解更多", "learn_more": "瞭解更多",
@@ -117,37 +117,37 @@
"username": "使用者名稱" "username": "使用者名稱"
}, },
"collection": { "collection": {
"created": "合已建立", "created": "合已建立",
"different_parent": "無法為父集合不同的集合重新排序", "different_parent": "Cannot reorder collection with different parent",
"edit": "編輯合", "edit": "編輯合",
"invalid_name": "請提供有效的合名稱", "invalid_name": "請提供有效的合名稱",
"invalid_root_move": "集合已在根目錄", "invalid_root_move": "Collection already in the root",
"moved": "移動成功", "moved": "Moved Successfully",
"my_collections": "我的合", "my_collections": "我的合",
"name": "我的新合", "name": "我的新合",
"name_length_insufficient": "合名稱至少要有 3 個字元。", "name_length_insufficient": "合名稱至少要有 3 個字元。",
"new": "建立合", "new": "建立合",
"order_changed": "集合順序已更新", "order_changed": "Collection Order Updated",
"renamed": "合已重新命名", "renamed": "合已重新命名",
"request_in_use": "請求正在使用中", "request_in_use": "請求正在使用中",
"save_as": "另存為", "save_as": "另存為",
"select": "選擇一個合", "select": "選擇一個合",
"select_location": "選擇位置", "select_location": "選擇位置",
"select_team": "選擇一個團隊", "select_team": "選擇一個團隊",
"team_collections": "團隊合" "team_collections": "團隊合"
}, },
"confirm": { "confirm": {
"exit_team": "您確定要離開此團隊嗎?", "exit_team": "您確定要離開此團隊嗎?",
"logout": "您確定要登出嗎?", "logout": "您確定要登出嗎?",
"remove_collection": "您確定要永久刪除該合嗎?", "remove_collection": "您確定要永久刪除該合嗎?",
"remove_environment": "您確定要永久刪除該環境嗎?", "remove_environment": "您確定要永久刪除該環境嗎?",
"remove_folder": "您確定要永久刪除該資料夾嗎?", "remove_folder": "您確定要永久刪除該資料夾嗎?",
"remove_history": "您確定要永久刪除全部歷史記錄嗎?", "remove_history": "您確定要永久刪除全部歷史記錄嗎?",
"remove_request": "您確定要永久刪除該請求嗎?", "remove_request": "您確定要永久刪除該請求嗎?",
"remove_team": "您確定要刪除該團隊嗎?", "remove_team": "您確定要刪除該團隊嗎?",
"remove_telemetry": "您確定要退出遙測服務嗎?", "remove_telemetry": "您確定要退出遙測服務嗎?",
"request_change": "您確定要捨棄目前的請求嗎?未儲存的變更將遺失。", "request_change": "您確定要捨棄當前請求嗎?未儲存的變更將遺失。",
"save_unsaved_tab": "您要儲存在此分頁做出的改動嗎?", "save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。" "sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
}, },
"count": { "count": {
@@ -160,13 +160,13 @@
}, },
"documentation": { "documentation": {
"generate": "產生文件", "generate": "產生文件",
"generate_message": "匯入 Hoppscotch 合以隨時隨地產生 API 文件。" "generate_message": "匯入 Hoppscotch 合以隨時隨地產生 API 文件。"
}, },
"empty": { "empty": {
"authorization": "該請求沒有使用任何授權", "authorization": "該請求沒有使用任何授權",
"body": "該請求沒有任何請求主體", "body": "該請求沒有任何請求主體",
"collection": "合為空", "collection": "合為空",
"collections": "合為空", "collections": "合為空",
"documentation": "連線到 GraphQL 端點以檢視文件", "documentation": "連線到 GraphQL 端點以檢視文件",
"endpoint": "端點不能留空", "endpoint": "端點不能留空",
"environments": "環境為空", "environments": "環境為空",
@@ -209,7 +209,7 @@
"browser_support_sse": "此瀏覽器似乎不支援 SSE。", "browser_support_sse": "此瀏覽器似乎不支援 SSE。",
"check_console_details": "檢查控制台日誌以獲悉詳情", "check_console_details": "檢查控制台日誌以獲悉詳情",
"curl_invalid_format": "cURL 格式不正確", "curl_invalid_format": "cURL 格式不正確",
"danger_zone": "危險地帶", "danger_zone": "Danger zone",
"delete_account": "您的帳號目前為這些團隊的擁有者:", "delete_account": "您的帳號目前為這些團隊的擁有者:",
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。", "delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
"empty_req_name": "空請求名稱", "empty_req_name": "空請求名稱",
@@ -277,38 +277,38 @@
"tests": "編寫測試指令碼以自動除錯。" "tests": "編寫測試指令碼以自動除錯。"
}, },
"hide": { "hide": {
"collection": "隱藏合面板", "collection": "隱藏合面板",
"more": "隱藏更多", "more": "隱藏更多",
"preview": "隱藏預覽", "preview": "隱藏預覽",
"sidebar": "隱藏側邊欄" "sidebar": "隱藏側邊欄"
}, },
"import": { "import": {
"collections": "匯入合", "collections": "匯入合",
"curl": "匯入 cURL", "curl": "匯入 cURL",
"failed": "匯入失敗", "failed": "匯入失敗",
"from_gist": "從 Gist 匯入", "from_gist": "從 Gist 匯入",
"from_gist_description": "從 Gist 網址匯入", "from_gist_description": "從 Gist 網址匯入",
"from_insomnia": "從 Insomnia 匯入", "from_insomnia": "從 Insomnia 匯入",
"from_insomnia_description": "從 Insomnia 合匯入", "from_insomnia_description": "從 Insomnia 合匯入",
"from_json": "從 Hoppscotch 匯入", "from_json": "從 Hoppscotch 匯入",
"from_json_description": "從 Hoppscotch 合檔匯入", "from_json_description": "從 Hoppscotch 合檔匯入",
"from_my_collections": "從我的合匯入", "from_my_collections": "從我的合匯入",
"from_my_collections_description": "從我的合檔匯入", "from_my_collections_description": "從我的合檔匯入",
"from_openapi": "從 OpenAPI 匯入", "from_openapi": "從 OpenAPI 匯入",
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入", "from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
"from_postman": "從 Postman 匯入", "from_postman": "從 Postman 匯入",
"from_postman_description": "從 Postman 合匯入", "from_postman_description": "從 Postman 合匯入",
"from_url": "從網址匯入", "from_url": "從網址匯入",
"gist_url": "輸入 Gist 網址", "gist_url": "輸入 Gist 網址",
"import_from_url_invalid_fetch": "無法從網址取得資料", "import_from_url_invalid_fetch": "無法從網址取得資料",
"import_from_url_invalid_file_format": "匯入合時發生錯誤", "import_from_url_invalid_file_format": "匯入合時發生錯誤",
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'", "import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
"import_from_url_success": "已匯入合", "import_from_url_success": "已匯入合",
"json_description": "從 Hoppscotch 合 JSON 檔匯入合", "json_description": "從 Hoppscotch 合 JSON 檔匯入合",
"title": "匯入" "title": "匯入"
}, },
"layout": { "layout": {
"collapse_collection": "隱藏或顯示合", "collapse_collection": "隱藏或顯示合",
"collapse_sidebar": "隱藏或顯示側邊欄", "collapse_sidebar": "隱藏或顯示側邊欄",
"column": "垂直版面", "column": "垂直版面",
"name": "配置", "name": "配置",
@@ -316,8 +316,8 @@
"zen_mode": "專注模式" "zen_mode": "專注模式"
}, },
"modal": { "modal": {
"close_unsaved_tab": "您有未儲存的改動", "close_unsaved_tab": "You have unsaved changes",
"collections": "合", "collections": "合",
"confirm": "確認", "confirm": "確認",
"edit_request": "編輯請求", "edit_request": "編輯請求",
"import_export": "匯入/匯出" "import_export": "匯入/匯出"
@@ -374,9 +374,9 @@
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。", "email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
"no_permission": "您沒有權限執行此操作。", "no_permission": "您沒有權限執行此操作。",
"owner": "擁有者", "owner": "擁有者",
"owner_description": "擁有者可以新增、編輯和刪除請求、合和團隊成員。", "owner_description": "擁有者可以新增、編輯和刪除請求、合和團隊成員。",
"roles": "角色", "roles": "角色",
"roles_description": "角色用來控制對共用合的存取權。", "roles_description": "角色用來控制對共用合的存取權。",
"updated": "已更新個人檔案", "updated": "已更新個人檔案",
"viewer": "檢視者", "viewer": "檢視者",
"viewer_description": "檢視者只能檢視和使用請求。" "viewer_description": "檢視者只能檢視和使用請求。"
@@ -396,8 +396,8 @@
"text": "文字" "text": "文字"
}, },
"copy_link": "複製連結", "copy_link": "複製連結",
"different_collection": "無法重新排列來自不同集合的請求", "different_collection": "Cannot reorder requests from different collections",
"duplicated": "已複製請求", "duplicated": "Request duplicated",
"duration": "持續時間", "duration": "持續時間",
"enter_curl": "輸入 cURL", "enter_curl": "輸入 cURL",
"generate_code": "產生程式碼", "generate_code": "產生程式碼",
@@ -405,10 +405,10 @@
"header_list": "請求標頭列表", "header_list": "請求標頭列表",
"invalid_name": "請提供請求名稱", "invalid_name": "請提供請求名稱",
"method": "方法", "method": "方法",
"moved": "已移動請求", "moved": "Request moved",
"name": "請求名稱", "name": "請求名稱",
"new": "新請求", "new": "新請求",
"order_changed": "已更新請求順序", "order_changed": "Request Order Updated",
"override": "覆寫", "override": "覆寫",
"override_help": "在標頭設置 <kbd>Content-Type</kbd>", "override_help": "在標頭設置 <kbd>Content-Type</kbd>",
"overriden": "已覆寫", "overriden": "已覆寫",
@@ -432,7 +432,7 @@
"view_my_links": "檢視我的連結" "view_my_links": "檢視我的連結"
}, },
"response": { "response": {
"audio": "音訊", "audio": "Audio",
"body": "回應本體", "body": "回應本體",
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)", "filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
"headers": "回應標頭", "headers": "回應標頭",
@@ -446,7 +446,7 @@
"status": "狀態", "status": "狀態",
"time": "時間", "time": "時間",
"title": "回應", "title": "回應",
"video": "視訊", "video": "Video",
"waiting_for_connection": "等待連線", "waiting_for_connection": "等待連線",
"xml": "XML" "xml": "XML"
}, },
@@ -494,7 +494,7 @@
"short_codes_description": "我們為您打造的快捷碼。", "short_codes_description": "我們為您打造的快捷碼。",
"sidebar_on_left": "左側邊欄", "sidebar_on_left": "左側邊欄",
"sync": "同步", "sync": "同步",
"sync_collections": "合", "sync_collections": "合",
"sync_description": "這些設定會同步到雲端。", "sync_description": "這些設定會同步到雲端。",
"sync_environments": "環境", "sync_environments": "環境",
"sync_history": "歷史", "sync_history": "歷史",
@@ -551,7 +551,7 @@
"previous_method": "選擇上一個方法", "previous_method": "選擇上一個方法",
"put_method": "選擇 PUT 方法", "put_method": "選擇 PUT 方法",
"reset_request": "重置請求", "reset_request": "重置請求",
"save_to_collections": "儲存到合", "save_to_collections": "儲存到合",
"send_request": "傳送請求", "send_request": "傳送請求",
"title": "請求" "title": "請求"
}, },
@@ -570,7 +570,7 @@
}, },
"show": { "show": {
"code": "顯示程式碼", "code": "顯示程式碼",
"collection": "顯示合面板", "collection": "顯示合面板",
"more": "顯示更多", "more": "顯示更多",
"sidebar": "顯示側邊欄" "sidebar": "顯示側邊欄"
}, },
@@ -639,9 +639,9 @@
"tab": { "tab": {
"authorization": "授權", "authorization": "授權",
"body": "請求本體", "body": "請求本體",
"collections": "合", "collections": "合",
"documentation": "幫助文件", "documentation": "幫助文件",
"environments": "環境", "environments": "Environments",
"headers": "請求標頭", "headers": "請求標頭",
"history": "歷史記錄", "history": "歷史記錄",
"mqtt": "MQTT", "mqtt": "MQTT",
@@ -666,7 +666,7 @@
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。", "email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
"exit": "退出團隊", "exit": "退出團隊",
"exit_disabled": "團隊擁有者無法退出團隊", "exit_disabled": "團隊擁有者無法退出團隊",
"invalid_coll_id": "集合 ID 無效", "invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "電子信箱格式無效", "invalid_email_format": "電子信箱格式無效",
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。", "invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
"invalid_invite_link": "邀請連結無效", "invalid_invite_link": "邀請連結無效",
@@ -690,21 +690,21 @@
"member_removed": "使用者已移除", "member_removed": "使用者已移除",
"member_role_updated": "使用者角色已更新", "member_role_updated": "使用者角色已更新",
"members": "成員", "members": "成員",
"more_members": "還有 {count} ", "more_members": "+{count} more",
"name_length_insufficient": "團隊名稱至少為 6 個字元", "name_length_insufficient": "團隊名稱至少為 6 個字元",
"name_updated": "團隊名稱已更新", "name_updated": "團隊名稱已更新",
"new": "新團隊", "new": "新團隊",
"new_created": "已建立新團隊", "new_created": "已建立新團隊",
"new_name": "我的新團隊", "new_name": "我的新團隊",
"no_access": "您沒有編輯合的許可權", "no_access": "您沒有編輯合的許可權",
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。", "no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
"no_request_found": "找不到請求。", "no_request_found": "Request not found.",
"not_found": "找不到團隊。請聯絡您的團隊擁有者。", "not_found": "找不到團隊。請聯絡您的團隊擁有者。",
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。", "not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
"parent_coll_move": "無法將集合移動至子集合", "parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "待定邀請", "pending_invites": "待定邀請",
"permissions": "許可權", "permissions": "許可權",
"same_target_destination": "目標和目的地相同", "same_target_destination": "Same target and destination",
"saved": "團隊已儲存", "saved": "團隊已儲存",
"select_a_team": "選擇團隊", "select_a_team": "選擇團隊",
"title": "團隊", "title": "團隊",
@@ -734,9 +734,9 @@
"url": "網址" "url": "網址"
}, },
"workspace": { "workspace": {
"change": "切換工作區", "change": "Change workspace",
"personal": "我的工作區", "personal": "My Workspace",
"team": "團隊工作區", "team": "Team Workspace",
"title": "工作區" "title": "Workspaces"
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "@hoppscotch/common", "name": "@hoppscotch/common",
"private": true, "private": true,
"version": "2023.8.0", "version": "2023.4.7",
"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",
@@ -22,141 +22,137 @@
}, },
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.9.0", "@codemirror/autocomplete": "^6.0.3",
"@codemirror/commands": "^6.2.4", "@codemirror/commands": "^6.0.1",
"@codemirror/lang-javascript": "^6.1.9", "@codemirror/lang-javascript": "^6.0.1",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-xml": "^6.0.2", "@codemirror/lang-xml": "^6.0.0",
"@codemirror/language": "^6.9.0", "@codemirror/language": "^6.2.0",
"@codemirror/legacy-modes": "^6.3.3", "@codemirror/legacy-modes": "^6.1.0",
"@codemirror/lint": "^6.4.0", "@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.5.1", "@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.1", "@codemirror/state": "^6.1.0",
"@codemirror/view": "^6.16.0", "@codemirror/view": "^6.0.2",
"@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.9",
"@hoppscotch/codemirror-lang-graphql": "workspace:^", "@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^", "@hoppscotch/data": "workspace:^",
"@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.6", "@lezer/highlight": "^1.0.0",
"@sentry/tracing": "^7.64.0", "@sentry/tracing": "^7.13.0",
"@sentry/vue": "^7.64.0", "@sentry/vue": "^7.13.0",
"@urql/core": "^4.1.1", "@urql/core": "^2.5.0",
"@urql/devtools": "^2.0.3", "@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6", "@urql/exchange-auth": "^0.1.7",
"@urql/exchange-graphcache": "^6.3.2", "@urql/exchange-graphcache": "^4.4.3",
"@vitejs/plugin-legacy": "^4.1.1", "@vitejs/plugin-legacy": "^2.3.0",
"@vueuse/core": "^10.3.0", "@vueuse/core": "^8.9.4",
"@vueuse/head": "^1.3.1", "@vueuse/head": "^0.7.9",
"acorn-walk": "^8.2.0", "acorn-walk": "^8.2.0",
"axios": "^1.4.0", "axios": "^0.21.4",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"dioc": "workspace:^", "dioc": "workspace:^",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"events": "^3.3.0", "events": "^3.3.0",
"fp-ts": "^2.16.1", "fp-ts": "^2.12.1",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"globalthis": "^1.0.3", "globalthis": "^1.0.3",
"graphql": "^16.8.0", "graphql": "^15.5.0",
"graphql-language-service-interface": "^2.9.1", "graphql-language-service-interface": "^2.9.1",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"httpsnippet": "^3.0.1", "httpsnippet": "^2.0.0",
"insomnia-importers": "^3.6.0", "insomnia-importers": "^3.3.0",
"io-ts": "^2.2.20", "io-ts": "^2.2.16",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonpath-plus": "^7.2.0", "jsonpath-plus": "^7.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lossless-json": "^2.0.11", "lossless-json": "^2.0.8",
"minisearch": "^6.1.0", "minisearch": "^6.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0", "paho-mqtt": "^1.1.0",
"path": "^0.12.7", "path": "^0.12.7",
"postman-collection": "^4.2.0", "postman-collection": "^4.1.4",
"process": "^0.11.10", "process": "^0.11.10",
"qs": "^6.11.2", "qs": "^6.10.3",
"rxjs": "^7.8.1", "rxjs": "^7.5.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",
"socketio-wildcard": "^2.0.0", "socketio-wildcard": "^2.0.0",
"splitpanes": "^3.1.5", "splitpanes": "^3.1.1",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"tern": "^0.24.3", "tern": "^0.24.3",
"timers": "^0.1.1", "timers": "^0.1.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"url": "^0.11.1", "url": "^0.11.0",
"util": "^0.12.5", "util": "^0.12.4",
"uuid": "^9.0.0", "uuid": "^8.3.2",
"vue": "^3.3.4", "vue": "^3.2.25",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-pdf-embed": "^1.1.6", "vue-pdf-embed": "^1.1.4",
"vue-router": "^4.2.4", "vue-router": "^4.0.16",
"vue-tippy": "6.3.1", "vue-tippy": "6.0.0-alpha.58",
"vuedraggable-es": "^4.1.1", "vuedraggable-es": "^4.1.1",
"wonka": "^6.3.4", "wonka": "^4.0.15",
"workbox-window": "^7.0.0", "workbox-window": "^6.5.4",
"xml-formatter": "^3.5.0", "xml-formatter": "^3.4.1",
"yargs-parser": "^21.1.1" "yargs-parser": "^21.1.1"
}, },
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-modules-polyfill": "^0.1.4",
"@graphql-codegen/add": "^5.0.0", "@graphql-codegen/add": "^3.2.0",
"@graphql-codegen/cli": "^5.0.0", "@graphql-codegen/cli": "^2.8.0",
"@graphql-codegen/typed-document-node": "^5.0.1", "@graphql-codegen/typed-document-node": "^2.3.1",
"@graphql-codegen/typescript": "^4.0.1", "@graphql-codegen/typescript": "^2.7.1",
"@graphql-codegen/typescript-operations": "^4.0.1", "@graphql-codegen/typescript-operations": "^2.5.1",
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5", "@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
"@graphql-codegen/urql-introspection": "^2.2.1", "@graphql-codegen/urql-introspection": "^2.2.0",
"@graphql-typed-document-node/core": "^3.2.0", "@graphql-typed-document-node/core": "^3.1.1",
"@iconify-json/lucide": "^1.1.119", "@iconify-json/lucide": "^1.1.109",
"@intlify/vite-plugin-vue-i18n": "^7.0.0", "@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1", "@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.3.3", "@rushstack/eslint-patch": "^1.1.4",
"@types/har-format": "^1.2.12",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.8", "@types/lodash-es": "^4.17.6",
"@types/lossless-json": "^1.0.1", "@types/lossless-json": "^1.0.1",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/paho-mqtt": "^1.0.7", "@types/paho-mqtt": "^1.0.6",
"@types/postman-collection": "^3.5.7", "@types/postman-collection": "^3.5.7",
"@types/splitpanes": "^2.2.1", "@types/splitpanes": "^2.2.1",
"@types/uuid": "^9.0.2", "@types/uuid": "^8.3.4",
"@types/yargs-parser": "^21.0.0", "@types/yargs-parser": "^21.0.0",
"@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^6.4.0", "@typescript-eslint/parser": "^5.19.0",
"@vitejs/plugin-vue": "^4.3.1", "@vitejs/plugin-vue": "^3.1.0",
"@vue/compiler-sfc": "^3.3.4", "@vue/compiler-sfc": "^3.2.39",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.1",
"@vue/runtime-core": "^3.3.4", "@vue/runtime-core": "^3.2.39",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1", "dotenv": "^16.0.3",
"eslint": "^8.47.0", "eslint": "^8.24.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.5.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3", "openapi-types": "^12.0.0",
"rollup-plugin-polyfill-node": "^0.12.0", "rollup-plugin-polyfill-node": "^0.10.1",
"sass": "^1.66.0", "sass": "^1.53.0",
"typescript": "^5.1.6", "typescript": "^4.5.4",
"unplugin-fonts": "^1.0.3", "unplugin-icons": "^0.14.9",
"unplugin-icons": "^0.16.5", "unplugin-vue-components": "^0.21.0",
"unplugin-vue-components": "^0.25.1", "vite": "^3.1.4",
"vite": "^4.4.9", "vite-plugin-checker": "^0.5.1",
"vite-plugin-checker": "^0.6.1", "vite-plugin-fonts": "^0.6.0",
"vite-plugin-html-config": "^1.0.11", "vite-plugin-html-config": "^1.0.10",
"vite-plugin-inspect": "^0.7.38", "vite-plugin-inspect": "^0.7.4",
"vite-plugin-pages": "^0.31.0", "vite-plugin-pages": "^0.26.0",
"vite-plugin-pages-sitemap": "^1.6.1", "vite-plugin-pages-sitemap": "^1.4.5",
"vite-plugin-pwa": "^0.16.4", "vite-plugin-pwa": "^0.13.1",
"vite-plugin-vue-layouts": "^0.8.0", "vite-plugin-vue-layouts": "^0.7.0",
"vite-plugin-windicss": "^1.9.1", "vite-plugin-windicss": "^1.8.8",
"vitest": "^0.34.2", "vitest": "^0.32.2",
"vue-tsc": "^1.8.8", "vue-tsc": "^0.38.2",
"windicss": "^3.5.6" "windicss": "^3.5.6"
} }
} }

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="#6366f1" 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: 389 B

View File

@@ -18,7 +18,6 @@ import { HOPP_MODULES } from "@modules/."
import { isLoadingInitialRoute } from "@modules/router" import { isLoadingInitialRoute } from "@modules/router"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { APP_IS_IN_DEV_MODE } from "@helpers/dev" import { APP_IS_IN_DEV_MODE } from "@helpers/dev"
import { platform } from "./platform"
const t = useI18n() const t = useI18n()
@@ -46,5 +45,4 @@ if (APP_IS_IN_DEV_MODE) {
// Run module root component setup code // Run module root component setup code
HOPP_MODULES.forEach((mod) => mod.onRootSetup?.()) HOPP_MODULES.forEach((mod) => mod.onRootSetup?.())
platform.addedHoppModules?.forEach((mod) => mod.onRootSetup?.())
</script> </script>

View File

@@ -1,11 +1,11 @@
/* eslint-disable */ // generated by unplugin-vue-components
/* prettier-ignore */ // We suggest you to commit this file into source control
// @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' { declare module '@vue/runtime-core' {
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']
@@ -14,7 +14,6 @@ declare module 'vue' {
AppFooter: typeof import('./components/app/Footer.vue')['default'] AppFooter: typeof import('./components/app/Footer.vue')['default']
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default'] AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default'] AppHeader: typeof import('./components/app/Header.vue')['default']
AppInspection: typeof import('./components/app/Inspection.vue')['default']
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default'] AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
AppLogo: typeof import('./components/app/Logo.vue')['default'] AppLogo: typeof import('./components/app/Logo.vue')['default']
AppOptions: typeof import('./components/app/Options.vue')['default'] AppOptions: typeof import('./components/app/Options.vue')['default']
@@ -24,14 +23,10 @@ declare module 'vue' {
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default'] AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default'] AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default'] AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSocial: typeof import('./components/app/Social.vue')['default']
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default'] AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default'] AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default'] AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default'] AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default'] AppSupport: typeof import('./components/app/Support.vue')['default']
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default'] ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default'] ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
@@ -73,18 +68,12 @@ declare module 'vue' {
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default'] FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default'] GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
GraphqlField: typeof import('./components/graphql/Field.vue')['default'] GraphqlField: typeof import('./components/graphql/Field.vue')['default']
GraphqlHeaders: typeof import('./components/graphql/Headers.vue')['default']
GraphqlQuery: typeof import('./components/graphql/Query.vue')['default']
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default'] GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default'] GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default'] GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default'] GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
GraphqlType: typeof import('./components/graphql/Type.vue')['default'] GraphqlType: typeof import('./components/graphql/Type.vue')['default']
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default'] GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
History: typeof import('./components/history/index.vue')['default'] History: typeof import('./components/history/index.vue')['default']
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default'] HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default'] HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
@@ -96,22 +85,17 @@ declare module 'vue' {
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand'] HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip'] HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'] HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'] HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'] HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder'] HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing'] HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup'] HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver'] HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'] HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab'] HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs'] HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow'] HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows'] HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default'] HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
@@ -133,17 +117,14 @@ declare module 'vue' {
HttpResponse: typeof import('./components/http/Response.vue')['default'] HttpResponse: typeof import('./components/http/Response.vue')['default']
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default'] HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default'] HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
HttpTestResult: typeof import('./components/http/TestResult.vue')['default'] HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default'] HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default'] HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default'] HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
HttpTests: typeof import('./components/http/Tests.vue')['default'] HttpTests: typeof import('./components/http/Tests.vue')['default']
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default'] HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default'] IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'] IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -156,8 +137,6 @@ declare module 'vue' {
IconLucideRss: typeof import('~icons/lucide/rss')['default'] IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default'] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default'] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default'] LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
@@ -177,8 +156,6 @@ declare module 'vue' {
RealtimeLog: typeof import('./components/realtime/Log.vue')['default'] RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default'] RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default'] RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default'] SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default'] SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default'] SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
@@ -190,7 +167,6 @@ declare module 'vue' {
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'] SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default'] 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']
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default'] SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
@@ -205,8 +181,8 @@ declare module 'vue' {
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default'] SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default'] SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default'] SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default'] SmartTree: typeof import('./components/smart/Tree.vue')['default']
SmartTreeBranch: typeof import('./../../hoppscotch-ui/src/components/smart/TreeBranch.vue')['default'] SmartTreeBranch: typeof import('./components/smart/TreeBranch.vue')['default']
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default'] SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default'] SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
TabPrimary: typeof import('./components/tab/Primary.vue')['default'] TabPrimary: typeof import('./components/tab/Primary.vue')['default']
@@ -222,4 +198,5 @@ declare module 'vue' {
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,53 +2,16 @@
<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 { pipe } from "fp-ts/function" import { defineActionHandler } from "~/helpers/actions"
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
}) })
@@ -60,9 +23,4 @@ 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

@@ -76,7 +76,6 @@
} }
" "
/> />
<!--
<HoppSmartItem <HoppSmartItem
ref="chat" ref="chat"
:icon="IconMessageCircle" :icon="IconMessageCircle"
@@ -89,34 +88,20 @@
} }
" "
/> />
--> <HoppSmartItem
<template :icon="IconGift"
v-for="footerItem in platform.ui?.additionalFooterMenuItems" :label="`${t('app.whats_new')}`"
:key="footerItem.id" to="https://docs.hoppscotch.io/documentation/changelog"
> blank
<template v-if="footerItem.action.type === 'link'"> @click="hide()"
<HoppSmartItem />
:icon="footerItem.icon" <HoppSmartItem
:label="footerItem.text(t)" :icon="IconActivity"
:to="footerItem.action.href" :label="t('app.status')"
blank to="https://status.hoppscotch.io"
@click="hide()" blank
/> @click="hide()"
</template> />
<HoppSmartItem
v-else
:icon="footerItem.icon"
:label="footerItem.text(t)"
blank
@click="
() => {
// @ts-expect-error TypeScript not understanding the type
footerItem.action.do()
hide()
}
"
/>
</template>
<hr /> <hr />
<HoppSmartItem <HoppSmartItem
:icon="IconGithub" :icon="IconGithub"
@@ -222,11 +207,15 @@ import IconColumns from "~icons/lucide/columns"
import IconSidebarOpen from "~icons/lucide/sidebar-open" import IconSidebarOpen from "~icons/lucide/sidebar-open"
import IconShieldCheck from "~icons/lucide/shield-check" import IconShieldCheck from "~icons/lucide/shield-check"
import IconBook from "~icons/lucide/book" import IconBook from "~icons/lucide/book"
import IconMessageCircle from "~icons/lucide/message-circle"
import IconGift from "~icons/lucide/gift"
import IconActivity from "~icons/lucide/activity"
import IconGithub from "~icons/lucide/github" import IconGithub from "~icons/lucide/github"
import IconTwitter from "~icons/lucide/twitter" import IconTwitter from "~icons/lucide/twitter"
import IconUserPlus from "~icons/lucide/user-plus" import IconUserPlus from "~icons/lucide/user-plus"
import IconLock from "~icons/lucide/lock" import IconLock from "~icons/lucide/lock"
import IconLifeBuoy from "~icons/lucide/life-buoy" import IconLifeBuoy from "~icons/lucide/life-buoy"
import { showChat } from "@modules/crisp"
import { useSetting } from "@composables/settings" import { useSetting } from "@composables/settings"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
@@ -273,6 +262,10 @@ const nativeShare = () => {
} }
} }
const chatWithUs = () => {
showChat()
}
const showDeveloperOptionModal = () => { const showDeveloperOptionModal = () => {
if (currentUser.value) { if (currentUser.value) {
showDeveloperOptions.value = true showDeveloperOptions.value = true

View File

@@ -18,14 +18,14 @@
</div> </div>
<div class="inline-flex items-center justify-center flex-1 space-x-2"> <div class="inline-flex items-center justify-center flex-1 space-x-2">
<button <button
class="flex flex-1 items-center justify-between px-2 py-1 self-stretch bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-60 hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary" class="flex flex-1 items-center justify-between px-2 py-1 bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-xs hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
@click="invokeAction('modals.search.toggle')" @click="invokeAction('modals.search.toggle')"
> >
<span class="inline-flex flex-1 items-center"> <span class="inline-flex flex-1 items-center">
<icon-lucide-search class="mr-2 svg-icons" /> <icon-lucide-search class="mr-2 svg-icons" />
{{ t("app.search") }} {{ t("app.search") }}
</span> </span>
<span class="flex space-x-1"> <span class="flex">
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd> <kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
<kbd class="shortcut-key">K</kbd> <kbd class="shortcut-key">K</kbd>
</span> </span>
@@ -254,10 +254,8 @@ import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { onLoggedIn } from "~/composables/auth" import { onLoggedIn } from "~/composables/auth"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { getPlatformSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { useToast } from "~/composables/toast"
const t = useI18n() const t = useI18n()
const toast = useToast()
/** /**
* Once the PWA code is initialized, this holds a method * Once the PWA code is initialized, this holds a method
@@ -374,8 +372,6 @@ const handleTeamEdit = () => {
editingTeamID.value = workspace.value.teamID editingTeamID.value = workspace.value.teamID
editingTeamName.value = { name: selectedTeam.value.name } editingTeamName.value = { name: selectedTeam.value.name }
displayModalEdit(true) displayModalEdit(true)
} else {
noPermission()
} }
} }
@@ -386,19 +382,6 @@ const settings = ref<any | null>(null)
const logout = ref<any | null>(null) const logout = ref<any | null>(null)
const accountActions = ref<any | null>(null) const accountActions = ref<any | null>(null)
defineActionHandler("modals.team.edit", handleTeamEdit)
defineActionHandler("modals.team.invite", () => {
if (
selectedTeam.value?.myRole === "OWNER" ||
selectedTeam.value?.myRole === "EDITOR"
) {
inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
} else {
noPermission()
}
})
defineActionHandler( defineActionHandler(
"user.login", "user.login",
() => { () => {
@@ -406,8 +389,4 @@ defineActionHandler(
}, },
computed(() => !currentUser.value) computed(() => !currentUser.value)
) )
const noPermission = () => {
toast.error(`${t("profile.no_permission")}`)
}
</script> </script>

View File

@@ -1,112 +0,0 @@
<template>
<div v-if="inspectionResults && inspectionResults.length > 0">
<tippy interactive trigger="click" theme="popover">
<div class="flex justify-center items-center flex-1 flex-col">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconAlertTriangle"
:class="severityColor(getHighestSeverity.severity)"
:title="t('inspections.description')"
/>
</div>
<template #content="{ hide }">
<div class="flex flex-col space-y-2 items-start flex-1">
<div
class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
>
<span class="flex items-center flex-1">
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
<span class="font-bold">
{{ t("inspections.title") }}
</span>
</span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/inspections"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
</div>
<div
v-for="(inspector, index) in inspectionResults"
:key="index"
class="flex self-stretch max-w-md w-full"
>
<div
class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
>
<span
v-if="inspector.text.type === 'text'"
class="flex-1 px-3 py-2"
>
{{ inspector.text.text }}
<HoppSmartLink
blank
:to="inspector.doc.link"
class="text-accent hover:text-accentDark transition"
>
{{ inspector.doc.text }}
<icon-lucide-arrow-up-right class="svg-icons" />
</HoppSmartLink>
</span>
<span v-if="inspector.action" class="flex p-2 space-x-2">
<HoppButtonSecondary
:label="inspector.action.text"
outline
filled
@click="
() => {
inspector.action?.apply()
hide()
}
"
/>
</span>
</div>
</div>
</div>
</template>
</tippy>
</div>
</template>
<script lang="ts" setup>
import { InspectorResult } from "~/services/inspection"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import IconHelpCircle from "~icons/lucide/help-circle"
import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
const props = defineProps<{
inspectionResults: InspectorResult[] | undefined
}>()
const getHighestSeverity = computed(() => {
if (props.inspectionResults) {
return props.inspectionResults.reduce(
(prev, curr) => {
return prev.severity > curr.severity ? prev : curr
},
{ severity: 0 }
)
} else {
return { severity: 0 }
}
})
const severityColor = (severity: number) => {
switch (severity) {
case 1:
return "!text-green-500 hover:!text-green-600"
case 2:
return "!text-yellow-500 hover:!text-yellow-600"
case 3:
return "!text-red-500 hover:!text-red-600"
default:
return "!text-gray-500 hover:!text-gray-600"
}
}
</script>

View File

@@ -8,41 +8,91 @@
{{ t("settings.interceptor_description") }} {{ t("settings.interceptor_description") }}
</p> </p>
</div> </div>
<HoppSmartRadioGroup
<div> v-model="interceptorSelection"
<div :radios="interceptors"
v-for="interceptor in interceptors" />
:key="interceptor.interceptorID" <div
class="flex flex-col" v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
> class="flex space-x-2"
<HoppSmartRadio >
:value="interceptor.interceptorID" <HoppButtonSecondary
:label="unref(interceptor.name(t))" to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
:selected="interceptorSelection === interceptor.interceptorID" blank
@change="interceptorSelection = interceptor.interceptorID" :icon="IconChrome"
/> label="Chrome"
outline
<component class="!flex-1"
:is="interceptor.selectorSubtitle" />
v-if="interceptor.selectorSubtitle" <HoppButtonSecondary
/> to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
</div> blank
:icon="IconFirefox"
label="Firefox"
outline
class="!flex-1"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconChrome from "~icons/brands/chrome"
import IconFirefox from "~icons/brands/firefox"
import { computed } from "vue"
import { applySetting, toggleSetting } from "~/newstore/settings"
import { useSetting } from "@composables/settings"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useService } from "dioc/vue" import { useReadonlyStream } from "@composables/stream"
import { Ref, unref } from "vue" import { extensionStatus$ } from "~/newstore/HoppExtension"
import { InterceptorService } from "~/services/interceptor.service"
const t = useI18n() const t = useI18n()
const interceptorService = useService(InterceptorService) const PROXY_ENABLED = useSetting("PROXY_ENABLED")
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
const interceptorSelection = const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
interceptorService.currentInterceptorID as Ref<string>
const interceptors = interceptorService.availableInterceptors const extensionVersion = computed(() => {
return currentExtensionStatus.value === "available"
? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null
: null
})
const interceptors = computed(() => [
{ value: "BROWSER_ENABLED" as const, label: t("state.none") },
{ value: "PROXY_ENABLED" as const, label: t("settings.proxy") },
{
value: "EXTENSIONS_ENABLED" as const,
label:
`${t("settings.extensions")}: ` +
(extensionVersion.value !== null
? `v${extensionVersion.value.major}.${extensionVersion.value.minor}`
: t("settings.extension_ver_not_reported")),
},
])
type InterceptorMode = (typeof interceptors)["value"][number]["value"]
const interceptorSelection = computed<InterceptorMode>({
get() {
if (PROXY_ENABLED.value) return "PROXY_ENABLED"
if (EXTENSIONS_ENABLED.value) return "EXTENSIONS_ENABLED"
return "BROWSER_ENABLED"
},
set(val) {
if (val === "EXTENSIONS_ENABLED") {
applySetting("EXTENSIONS_ENABLED", true)
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
}
if (val === "PROXY_ENABLED") {
applySetting("PROXY_ENABLED", true)
if (EXTENSIONS_ENABLED.value) toggleSetting("EXTENSIONS_ENABLED")
}
if (val === "BROWSER_ENABLED") {
applySetting("PROXY_ENABLED", false)
applySetting("EXTENSIONS_ENABLED", false)
}
},
})
</script> </script>

View File

@@ -30,52 +30,134 @@
<h2 class="p-4 font-semibold font-bold text-secondaryDark"> <h2 class="p-4 font-semibold font-bold text-secondaryDark">
{{ t("support.title") }} {{ t("support.title") }}
</h2> </h2>
<template <HoppSmartItem
v-for="item in platform.ui?.additionalSupportOptionsMenuItems" :icon="IconBook"
:key="item.id" :label="t('app.documentation')"
> to="https://docs.hoppscotch.io"
<HoppSmartItem :description="t('support.documentation')"
v-if="item.action.type === 'link'" :info-icon="IconChevronRight"
:icon="item.icon" active
:label="item.text(t)" blank
:to="item.action.href" @click="hideModal()"
:description="item.subtitle(t)" />
:info-icon="IconChevronRight" <HoppSmartItem
active :icon="IconGift"
blank :label="t('app.whats_new')"
@click="hideModal()" to="https://docs.hoppscotch.io/documentation/changelog"
/> :description="t('support.changelog')"
<HoppSmartItem :info-icon="IconChevronRight"
v-else active
:icon="item.icon" blank
:label="item.text(t)" @click="hideModal()"
:description="item.subtitle(t)" />
:info-icon="IconChevronRight" <HoppSmartItem
active :icon="IconActivity"
@click=" :label="t('app.status')"
() => { to="https://status.hoppscotch.io"
// @ts-expect-error Typescript isn't able to understand blank
item.action.do() :description="t('app.status_description')"
hideModal() :info-icon="IconChevronRight"
} active
" @click="hideModal()"
/> />
</template> <HoppSmartItem
:icon="IconLock"
:label="`${t('app.terms_and_privacy')}`"
to="https://docs.hoppscotch.io/support/privacy"
blank
:description="t('app.terms_and_privacy')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
{{ t("settings.follow") }}
</h2>
<HoppSmartItem
:icon="IconDiscord"
:label="t('app.discord')"
to="https://hoppscotch.io/discord"
blank
:description="t('app.join_discord_community')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconTwitter"
:label="t('app.twitter')"
to="https://hoppscotch.io/twitter"
blank
:description="t('support.twitter')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconGithub"
:label="`${t('app.github')}`"
to="https://github.com/hoppscotch/hoppscotch"
blank
:description="t('support.github')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconMessageCircle"
:label="t('app.chat_with_us')"
:description="t('support.chat')"
:info-icon="IconChevronRight"
active
@click="chatWithUs()"
/>
<HoppSmartItem
:icon="IconUserPlus"
:label="`${t('app.invite')}`"
:description="t('shortcut.miscellaneous.invite')"
:info-icon="IconChevronRight"
active
@click="expandInvite()"
/>
<HoppSmartItem
v-if="navigatorShare"
v-tippy="{ theme: 'tooltip' }"
:icon="IconShare2"
:label="`${t('request.share')}`"
:description="t('request.share_description')"
:info-icon="IconChevronRight"
active
@click="nativeShare()"
/>
</div> </div>
<AppShare :show="showShare" @hide-modal="showShare = false" />
</template> </template>
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watch } from "vue" import { ref, watch } from "vue"
import IconSidebar from "~icons/lucide/sidebar" import IconSidebar from "~icons/lucide/sidebar"
import IconSidebarOpen from "~icons/lucide/sidebar-open" import IconSidebarOpen from "~icons/lucide/sidebar-open"
import IconBook from "~icons/lucide/book"
import IconGift from "~icons/lucide/gift"
import IconActivity from "~icons/lucide/activity"
import IconLock from "~icons/lucide/lock"
import IconDiscord from "~icons/brands/discord"
import IconTwitter from "~icons/brands/twitter"
import IconGithub from "~icons/lucide/github"
import IconMessageCircle from "~icons/lucide/message-circle"
import IconUserPlus from "~icons/lucide/user-plus"
import IconShare2 from "~icons/lucide/share-2"
import IconChevronRight from "~icons/lucide/chevron-right" import IconChevronRight from "~icons/lucide/chevron-right"
import { useSetting } from "@composables/settings" import { useSetting } from "@composables/settings"
import { defineActionHandler } from "~/helpers/actions"
import { showChat } from "@modules/crisp"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
const navigatorShare = !!navigator.share
const showShare = ref(false)
const ZEN_MODE = useSetting("ZEN_MODE") const ZEN_MODE = useSetting("ZEN_MODE")
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION") const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
@@ -92,10 +174,19 @@ defineProps<{
show: boolean show: boolean
}>() }>()
defineActionHandler("modals.share.toggle", () => {
showShare.value = !showShare.value
})
const emit = defineEmits<{ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const chatWithUs = () => {
showChat()
hideModal()
}
const expandNavigation = () => { const expandNavigation = () => {
EXPAND_NAVIGATION.value = !EXPAND_NAVIGATION.value EXPAND_NAVIGATION.value = !EXPAND_NAVIGATION.value
hideModal() hideModal()
@@ -106,6 +197,24 @@ const expandCollection = () => {
hideModal() hideModal()
} }
const expandInvite = () => {
showShare.value = true
}
const nativeShare = () => {
if (navigator.share) {
navigator
.share({
title: "Hoppscotch",
text: "Hoppscotch • Open source API development ecosystem - Helps you create requests faster, saving precious time on development.",
url: "https://hoppscotch.io",
})
.catch(console.error)
} else {
// fallback
}
}
const hideModal = () => { const hideModal = () => {
emit("hide-modal") emit("hide-modal")
} }

View File

@@ -18,18 +18,13 @@
:horizontal="COLUMN_LAYOUT" :horizontal="COLUMN_LAYOUT"
@resize="setPaneEvent($event, 'horizontal')" @resize="setPaneEvent($event, 'horizontal')"
> >
<Pane <Pane :size="PANE_MAIN_TOP_SIZE" class="flex flex-col !overflow-auto">
:size="PANE_MAIN_TOP_SIZE"
class="flex flex-col !overflow-auto"
min-size="25"
>
<slot name="primary" /> <slot name="primary" />
</Pane> </Pane>
<Pane <Pane
v-if="hasSecondary" v-if="hasSecondary"
:size="PANE_MAIN_BOTTOM_SIZE" :size="PANE_MAIN_BOTTOM_SIZE"
class="flex flex-col !overflow-auto" class="flex flex-col !overflow-auto"
min-size="25"
> >
<slot name="secondary" /> <slot name="secondary" />
</Pane> </Pane>
@@ -38,7 +33,7 @@
<Pane <Pane
v-if="SIDEBAR && hasSidebar" v-if="SIDEBAR && hasSidebar"
:size="PANE_SIDEBAR_SIZE" :size="PANE_SIDEBAR_SIZE"
min-size="25" min-size="20"
class="flex flex-col !overflow-auto bg-primaryContrast" class="flex flex-col !overflow-auto bg-primaryContrast"
> >
<slot name="sidebar" /> <slot name="sidebar" />
@@ -83,10 +78,10 @@ type PaneEvent = {
size: number size: number
} }
const PANE_MAIN_SIZE = ref(70) const PANE_MAIN_SIZE = ref(74)
const PANE_SIDEBAR_SIZE = ref(30) const PANE_SIDEBAR_SIZE = ref(26)
const PANE_MAIN_TOP_SIZE = ref(35) const PANE_MAIN_TOP_SIZE = ref(42)
const PANE_MAIN_BOTTOM_SIZE = ref(65) const PANE_MAIN_BOTTOM_SIZE = ref(58)
if (!COLUMN_LAYOUT.value) { if (!COLUMN_LAYOUT.value) {
PANE_MAIN_TOP_SIZE.value = 50 PANE_MAIN_TOP_SIZE.value = 50

View File

@@ -4,13 +4,15 @@
<div <div
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary" class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
> >
<HoppSmartInput <div class="flex flex-col px-6 py-4 border-b border-dividerLight">
v-model="filterText" <input
type="search" v-model="filterText"
styles="px-6 py-4 border-b border-dividerLight" type="search"
:placeholder="`${t('action.search')}`" autocomplete="off"
input-styles="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark" class="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
/> :placeholder="`${t('action.search')}`"
/>
</div>
</div> </div>
<div class="flex flex-col divide-y divide-dividerLight"> <div class="flex flex-col divide-y divide-dividerLight">
<HoppSmartPlaceholder <HoppSmartPlaceholder
@@ -19,7 +21,6 @@
> >
<icon-lucide-search class="pb-2 opacity-75 svg-icons" /> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<details <details
v-for="(sectionResults, sectionTitle) in shortcutsResults" v-for="(sectionResults, sectionTitle) in shortcutsResults"
v-else v-else

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col items-center justify-center text-secondaryLight"> <div class="flex flex-col items-center justify-center text-secondaryLight">
<div class="flex mb-4 space-x-2"> <div class="flex pb-4 my-4 space-x-2">
<div class="flex flex-col items-end space-y-4 text-right"> <div class="flex flex-col items-end space-y-4 text-right">
<span class="flex items-center flex-1"> <span class="flex items-center flex-1">
{{ t("shortcut.request.send_request") }} {{ t("shortcut.request.send_request") }}

View File

@@ -8,46 +8,89 @@
> >
<template #body> <template #body>
<div class="flex flex-col space-y-2"> <div class="flex flex-col space-y-2">
<template <HoppSmartItem
v-for="item in platform.ui?.additionalSupportOptionsMenuItems" :icon="IconBook"
:key="item.id" :label="t('app.documentation')"
> to="https://docs.hoppscotch.io"
<HoppSmartItem :description="t('support.documentation')"
v-if="item.action.type === 'link'" :info-icon="IconChevronRight"
:icon="item.icon" active
:label="item.text(t)" blank
:to="item.action.href" @click="hideModal()"
:description="item.subtitle(t)" />
:info-icon="IconChevronRight" <HoppSmartItem
active :icon="IconZap"
blank :label="t('app.keyboard_shortcuts')"
@click="hideModal()" :description="t('support.shortcuts')"
/> :info-icon="IconChevronRight"
<HoppSmartItem active
v-else @click="showShortcuts()"
:icon="item.icon" />
:label="item.text(t)" <HoppSmartItem
:description="item.subtitle(t)" :icon="IconGift"
:info-icon="IconChevronRight" :label="t('app.whats_new')"
active to="https://docs.hoppscotch.io/documentation/changelog"
@click=" :description="t('support.changelog')"
() => { :info-icon="IconChevronRight"
// @ts-expect-error Typescript isn't able to understand active
item.action.do() blank
hideModal() @click="hideModal()"
} />
" <HoppSmartItem
/> :icon="IconMessageCircle"
</template> :label="t('app.chat_with_us')"
:description="t('support.chat')"
:info-icon="IconChevronRight"
active
@click="chatWithUs()"
/>
<HoppSmartItem
:icon="IconGitHub"
:label="t('app.github')"
to="https://hoppscotch.io/github"
blank
:description="t('support.github')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconDiscord"
:label="t('app.join_discord_community')"
to="https://hoppscotch.io/discord"
blank
:description="t('support.community')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconTwitter"
:label="t('app.twitter')"
to="https://hoppscotch.io/twitter"
blank
:description="t('support.twitter')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
</div> </div>
</template> </template>
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconTwitter from "~icons/brands/twitter"
import IconDiscord from "~icons/brands/discord"
import IconGitHub from "~icons/lucide/github"
import IconMessageCircle from "~icons/lucide/message-circle"
import IconGift from "~icons/lucide/gift"
import IconZap from "~icons/lucide/zap"
import IconBook from "~icons/lucide/book"
import IconChevronRight from "~icons/lucide/chevron-right" import IconChevronRight from "~icons/lucide/chevron-right"
import { invokeAction } from "@helpers/actions"
import { showChat } from "@modules/crisp"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
@@ -59,6 +102,16 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const chatWithUs = () => {
showChat()
hideModal()
}
const showShortcuts = () => {
invokeAction("flyouts.keybinds.toggle")
hideModal()
}
const hideModal = () => { const hideModal = () => {
emit("hide-modal") emit("hide-modal")
} }

View File

@@ -80,11 +80,10 @@ const props = defineProps<{
active: boolean active: boolean
}>() }>()
const formattedShortcutKeys = computed( const formattedShortcutKeys = computed(() =>
() => props.entry.meta?.keyboardShortcut?.map((key) => {
props.entry.meta?.keyboardShortcut?.map((key) => { return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
return SPECIAL_KEY_CHARS[key] ?? capitalize(key) })
})
) )
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -1,65 +0,0 @@
<template>
<span class="flex flex-1 space-x-2 items-center">
<template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { graphqlCollectionStore } from "~/newstore/collections"
const props = defineProps<{
folderPath: string
}>()
const pathFolders = computed(() => {
try {
const folderIndicies = props.folderPath
.split("/")
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection<HoppGQLRequest>[] = []
let currentFolder =
graphqlCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
pathItems.push(folder)
currentFolder = folder
}
return pathItems
} catch (e) {
console.error(e)
return []
}
})
const request = computed(() => {
try {
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex
]
} catch (e) {
return null
}
})
</script>

View File

@@ -1,3 +0,0 @@
<template>
<IconLucideCheckCircle class="text-accent" />
</template>

View File

@@ -1,71 +0,0 @@
<template>
<span class="flex flex-1 items-center space-x-2">
<template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span
v-if="request"
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
:class="getMethodLabelColorClassOf(request)"
>
{{ request.method.toUpperCase() }}
</span>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { restCollectionStore } from "~/newstore/collections"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
const props = defineProps<{
folderPath: string
}>()
const pathFolders = computed(() => {
try {
const folderIndicies = props.folderPath
.split("/")
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection<HoppRESTRequest>[] = []
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
pathItems.push(folder)
currentFolder = folder
}
return pathItems
} catch (e) {
console.error(e)
return []
}
})
const request = computed(() => {
try {
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex
]
} catch (e) {
return null
}
})
</script>

View File

@@ -95,23 +95,6 @@ import {
import { isEqual } from "lodash-es" import { isEqual } from "lodash-es"
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher" import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher" import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
import {
EnvironmentsSpotlightSearcherService,
SwitchEnvSpotlightSearcherService,
} from "~/services/spotlight/searchers/environment.searcher"
import {
SwitchWorkspaceSpotlightSearcherService,
WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
const t = useI18n() const t = useI18n()
@@ -127,19 +110,6 @@ const spotlightService = useService(SpotlightService)
useService(HistorySpotlightSearcherService) useService(HistorySpotlightSearcherService)
useService(UserSpotlightSearcherService) useService(UserSpotlightSearcherService)
useService(NavigationSpotlightSearcherService)
useService(SettingsSpotlightSearcherService)
useService(CollectionsSpotlightSearcherService)
useService(MiscellaneousSpotlightSearcherService)
useService(TabSpotlightSearcherService)
useService(GeneralSpotlightSearcherService)
useService(ResponseSpotlightSearcherService)
useService(RequestSpotlightSearcherService)
useService(EnvironmentsSpotlightSearcherService)
useService(SwitchEnvSpotlightSearcherService)
useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService)
const search = ref("") const search = ref("")

View File

@@ -6,13 +6,21 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="editingName" <input
placeholder=" " id="selectLabelAdd"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
@submit="addNewCollection" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addNewCollection"
/>
<label for="selectLabelAdd">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
@@ -57,28 +65,28 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const editingName = ref("") const name = ref("")
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (!show) { if (!show) {
editingName.value = "" name.value = ""
} }
} }
) )
const addNewCollection = () => { const addNewCollection = () => {
if (!editingName.value) { if (!name.value) {
toast.error(t("collection.invalid_name")) toast.error(t("collection.invalid_name"))
return return
} }
emit("submit", editingName.value) emit("submit", name.value)
} }
const hideModal = () => { const hideModal = () => {
editingName.value = "" name.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -6,13 +6,21 @@
@close="emit('hide-modal')" @close="emit('hide-modal')"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="editingName" <input
placeholder=" " id="selectLabelAddFolder"
input-styles="floating-input" v-model="name"
:label="t('action.label')" v-focus
@submit="addFolder" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addFolder"
/>
<label for="selectLabelAddFolder">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
@@ -57,27 +65,27 @@ const emit = defineEmits<{
(e: "add-folder", name: string): void (e: "add-folder", name: string): void
}>() }>()
const editingName = ref("") const name = ref("")
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (!show) { if (!show) {
editingName.value = "" name.value = ""
} }
} }
) )
const addFolder = () => { const addFolder = () => {
if (editingName.value.trim() === "") { if (name.value.trim() === "") {
toast.error(t("folder.invalid_name")) toast.error(t("folder.invalid_name"))
return return
} }
emit("add-folder", editingName.value) emit("add-folder", name.value)
} }
const hideModal = () => { const hideModal = () => {
editingName.value = "" name.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -6,13 +6,19 @@
@close="$emit('hide-modal')" @close="$emit('hide-modal')"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="editingName" <input
placeholder=" " id="selectLabelAddRequest"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
@submit="addRequest" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addRequest"
/>
<label for="selectLabelAddRequest">{{ t("action.label") }}</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
@@ -58,23 +64,23 @@ const emit = defineEmits<{
(event: "add-request", name: string): void (event: "add-request", name: string): void
}>() }>()
const editingName = ref("") const name = ref("")
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
editingName.value = currentActiveTab.value.document.request.name name.value = currentActiveTab.value.document.request.name
} }
} }
) )
const addRequest = () => { const addRequest = () => {
if (editingName.value.trim() === "") { if (name.value.trim() === "") {
toast.error(`${t("error.empty_req_name")}`) toast.error(`${t("error.empty_req_name")}`)
return return
} }
emit("add-request", editingName.value) emit("add-request", name.value)
} }
const hideModal = () => { const hideModal = () => {

View File

@@ -6,13 +6,21 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="editingName" <input
placeholder=" " id="selectLabelEdit"
input-styles="floating-input" v-model="name"
:label="t('action.label')" v-focus
@submit="saveCollection" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveCollection"
/>
<label for="selectLabelEdit">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
@@ -59,26 +67,26 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const editingName = ref("") const name = ref("")
watch( watch(
() => props.editingCollectionName, () => props.editingCollectionName,
(newName) => { (newName) => {
editingName.value = newName name.value = newName
} }
) )
const saveCollection = () => { const saveCollection = () => {
if (editingName.value.trim() === "") { if (name.value.trim() === "") {
toast.error(t("collection.invalid_name")) toast.error(t("collection.invalid_name"))
return return
} }
emit("submit", editingName.value) emit("submit", name.value)
} }
const hideModal = () => { const hideModal = () => {
editingName.value = "" name.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -6,13 +6,21 @@
@close="emit('hide-modal')" @close="emit('hide-modal')"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="editingName" <input
placeholder=" " id="selectLabelEditFolder"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
@submit="editFolder" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="editFolder"
/>
<label for="selectLabelEditFolder">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
@@ -59,26 +67,26 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const editingName = ref("") const name = ref("")
watch( watch(
() => props.editingFolderName, () => props.editingFolderName,
(newName) => { (newName) => {
editingName.value = newName name.value = newName
} }
) )
const editFolder = () => { const editFolder = () => {
if (editingName.value.trim() === "") { if (name.value.trim() === "") {
toast.error(t("folder.invalid_name")) toast.error(t("folder.invalid_name"))
return return
} }
emit("submit", editingName.value) emit("submit", name.value)
} }
const hideModal = () => { const hideModal = () => {
editingName.value = "" name.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -6,13 +6,21 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="editingName" <input
placeholder=" " id="selectLabelEditReq"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
@submit="editRequest" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="editRequest"
/>
<label for="selectLabelEditReq">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
@@ -60,19 +68,19 @@ const emit = defineEmits<{
(e: "update:modelValue", value: string): void (e: "update:modelValue", value: string): void
}>() }>()
const editingName = useVModel(props, "modelValue") const name = useVModel(props, "modelValue")
const editRequest = () => { const editRequest = () => {
if (editingName.value.trim() === "") { if (name.value.trim() === "") {
toast.error(t("request.invalid_name")) toast.error(t("request.invalid_name"))
return return
} }
emit("submit", editingName.value) emit("submit", name.value)
} }
const hideModal = () => { const hideModal = () => {
editingName.value = "" name.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -32,7 +32,7 @@
</span> </span>
</div> </div>
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<HoppSmartTree :adapter="myAdapter"> <SmartTree :adapter="myAdapter">
<template <template
#content="{ node, toggleChildren, isOpen, highlightChildren }" #content="{ node, toggleChildren, isOpen, highlightChildren }"
> >
@@ -291,7 +291,7 @@
> >
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</template> </template>
</HoppSmartTree> </SmartTree>
</div> </div>
</div> </div>
</template> </template>
@@ -303,10 +303,7 @@ import IconHelpCircle from "~icons/lucide/help-circle"
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"
import { import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
ChildrenResult,
SmartTreeAdapter,
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"

View File

@@ -8,15 +8,21 @@
> >
<template #body> <template #body>
<div class="flex flex-col"> <div class="flex flex-col">
<HoppSmartInput <div class="relative flex">
v-model="requestName" <input
styles="relative flex" id="selectLabelSaveReq"
placeholder=" " v-model="requestName"
:label="t('request.name')" v-focus
input-styles="floating-input" class="input floating-input"
@submit="saveRequestAs" placeholder=" "
/> type="text"
autocomplete="off"
@keyup.enter="saveRequestAs"
/>
<label for="selectLabelSaveReq">
{{ t("request.name") }}
</label>
</div>
<label class="p-4"> <label class="p-4">
{{ t("collection.select_location") }} {{ t("collection.select_location") }}
</label> </label>
@@ -56,7 +62,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, reactive, ref, watch } from "vue" import { nextTick, reactive, ref, watch } from "vue"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { import {
HoppGQLRequest, HoppGQLRequest,
@@ -71,6 +77,7 @@ import {
updateTeamRequest, updateTeamRequest,
} from "~/helpers/backend/mutations/TeamRequest" } from "~/helpers/backend/mutations/TeamRequest"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { import {
@@ -81,9 +88,8 @@ import {
} from "~/newstore/collections" } from "~/newstore/collections"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core" import { computedWithControl } from "@vueuse/core"
import { currentActiveTab } from "~/helpers/rest/tab"
import { platform } from "~/platform" import { platform } from "~/platform"
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -101,12 +107,10 @@ const props = withDefaults(
defineProps<{ defineProps<{
show: boolean show: boolean
mode: "rest" | "graphql" mode: "rest" | "graphql"
request?: HoppRESTRequest | HoppGQLRequest | null
}>(), }>(),
{ {
show: false, show: false,
mode: "rest", mode: "rest",
request: null,
} }
) )
@@ -122,36 +126,22 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const gqlRequestName = computedWithControl( const gqlRequestName = useGQLRequestName()
() => activeGQLTab.value,
() => activeGQLTab.value.document.request.name
)
const restRequestName = computedWithControl( const restRequestName = computedWithControl(
() => activeRESTTab.value, () => currentActiveTab.value,
() => activeRESTTab.value.document.request.name () => currentActiveTab.value.document.request.name
) )
const reqName = computed(() => { const requestName = ref(
if (props.request) { props.mode === "rest" ? restRequestName.value : gqlRequestName.value
return props.request.name )
} else if (props.mode === "rest") {
return restRequestName.value
} else {
return gqlRequestName.value
}
})
const requestName = ref(reqName.value)
watch( watch(
() => [activeRESTTab.value, activeGQLTab.value], () => [currentActiveTab.value, gqlRequestName.value],
() => { () => {
if (props.mode === "rest") { if (props.mode === "rest") {
requestName.value = activeRESTTab.value?.document.request.name ?? "" requestName.value = currentActiveTab.value?.document.request.name ?? ""
} else { } else requestName.value = gqlRequestName.value
requestName.value = activeGQLTab.value?.document.request.name ?? ""
}
} }
) )
@@ -210,8 +200,8 @@ const saveRequestAs = async () => {
const requestUpdated = const requestUpdated =
props.mode === "rest" props.mode === "rest"
? cloneDeep(activeRESTTab.value.document.request) ? cloneDeep(currentActiveTab.value.document.request)
: cloneDeep(activeGQLTab.value.document.request) : cloneDeep(getGQLSession().request)
requestUpdated.name = requestName.value requestUpdated.name = requestName.value
@@ -224,7 +214,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
activeRESTTab.value.document = { currentActiveTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -251,7 +241,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
activeRESTTab.value.document = { currentActiveTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -279,7 +269,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
activeRESTTab.value.document = { currentActiveTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -439,7 +429,7 @@ const updateTeamCollectionOrFolder = (
(result) => { (result) => {
const { createRequestInCollection } = result const { createRequestInCollection } = result
activeRESTTab.value.document = { currentActiveTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -460,7 +450,7 @@ const updateTeamCollectionOrFolder = (
const requestSaved = () => { const requestSaved = () => {
toast.success(`${t("request.added")}`) toast.success(`${t("request.added")}`)
nextTick(() => { nextTick(() => {
activeRESTTab.value.document.isDirty = false currentActiveTab.value.document.isDirty = false
}) })
hideModal() hideModal()
} }

View File

@@ -46,7 +46,7 @@
</span> </span>
</div> </div>
<div class="flex flex-col overflow-hidden"> <div class="flex flex-col overflow-hidden">
<HoppSmartTree :adapter="teamAdapter"> <SmartTree :adapter="teamAdapter">
<template <template
#content="{ node, toggleChildren, isOpen, highlightChildren }" #content="{ node, toggleChildren, isOpen, highlightChildren }"
> >
@@ -311,7 +311,7 @@
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</div> </div>
</template> </template>
</HoppSmartTree> </SmartTree>
</div> </div>
</div> </div>
</template> </template>
@@ -326,10 +326,7 @@ import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { TeamCollection } from "~/helpers/teams/TeamCollection" import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { TeamRequest } from "~/helpers/teams/TeamRequest" import { TeamRequest } from "~/helpers/teams/TeamRequest"
import { import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
ChildrenResult,
SmartTreeAdapter,
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { HoppRESTRequest } from "@hoppscotch/data" import { HoppRESTRequest } from "@hoppscotch/data"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"

View File

@@ -6,13 +6,21 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="name" <input
placeholder=" " id="selectLabelGqlAdd"
input-styles="floating-input" v-model="name"
:label="t('action.label')" v-focus
@submit="addNewCollection" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addNewCollection"
/>
<label for="selectLabelGqlAdd">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">

View File

@@ -6,13 +6,21 @@
@close="$emit('hide-modal')" @close="$emit('hide-modal')"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="name" <input
placeholder=" " id="selectLabelGqlAddFolder"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
@submit="addFolder" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addFolder"
/>
<label for="selectLabelGqlAddFolder">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">

View File

@@ -6,13 +6,21 @@
@close="emit('hide-modal')" @close="emit('hide-modal')"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="editingName" <input
placeholder=" " id="selectLabelGqlAddRequest"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
@submit="addRequest" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="addRequest"
/>
<label for="selectLabelGqlAddRequest">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
@@ -36,7 +44,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 { currentActiveTab } from "~/helpers/graphql/tab" import { getGQLSession } from "~/newstore/GQLSession"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
@@ -57,24 +65,24 @@ const emit = defineEmits<{
): void ): void
}>() }>()
const editingName = ref("") const name = ref("")
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
editingName.value = currentActiveTab.value?.document.request.name name.value = getGQLSession().request.name
} }
} }
) )
const addRequest = () => { const addRequest = () => {
if (!editingName.value) { if (!name.value) {
toast.error(`${t("error.empty_req_name")}`) toast.error(`${t("error.empty_req_name")}`)
return return
} }
emit("add-request", { emit("add-request", {
name: editingName.value, name: name.value,
path: props.folderPath, path: props.folderPath,
}) })
hideModal() hideModal()

View File

@@ -37,7 +37,6 @@
@click=" @click="
emit('add-request', { emit('add-request', {
path: `${collectionIndex}`, path: `${collectionIndex}`,
index: collection.requests.length,
}) })
" "
/> />
@@ -220,7 +219,6 @@ import {
moveGraphqlRequest, moveGraphqlRequest,
} from "~/newstore/collections" } from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { getTabsRefTo } from "~/helpers/graphql/tab"
const props = defineProps({ const props = defineProps({
picked: { type: Object, default: null }, picked: { type: Object, default: null },
@@ -295,22 +293,6 @@ const removeCollection = () => {
emit("select", null) emit("select", null)
} }
const possibleTabs = getTabsRefTo((tab) => {
const ctx = tab.document.saveContext
if (!ctx) return false
return (
ctx.originLocation === "user-collection" &&
ctx.folderPath.startsWith(props.collectionIndex.toString())
)
})
for (const tab of possibleTabs) {
tab.value.document.saveContext = undefined
tab.value.document.isDirty = true
}
removeGraphqlCollection(props.collectionIndex, props.collection.id) removeGraphqlCollection(props.collectionIndex, props.collection.id)
toast.success(`${t("state.deleted")}`) toast.success(`${t("state.deleted")}`)
} }

View File

@@ -6,13 +6,21 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="editingName" <input
placeholder=" " id="selectLabelGqlEdit"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
@submit="saveCollection" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveCollection"
/>
<label for="selectLabelGqlEdit">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">
@@ -52,17 +60,17 @@ const emit = defineEmits<{
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const editingName = ref<string | null>() const name = ref<string | null>()
watch( watch(
() => props.editingCollectionName, () => props.editingCollectionName,
(val) => { (val) => {
editingName.value = val name.value = val
} }
) )
const saveCollection = () => { const saveCollection = () => {
if (!editingName.value) { if (!name.value) {
toast.error(`${t("collection.invalid_name")}`) toast.error(`${t("collection.invalid_name")}`)
return return
} }
@@ -70,7 +78,7 @@ const saveCollection = () => {
// TODO: Better typechecking here ? // TODO: Better typechecking here ?
const collectionUpdated = { const collectionUpdated = {
...(props.editingCollection as any), ...(props.editingCollection as any),
name: editingName.value, name: name.value,
} }
editGraphqlCollection(props.editingCollectionIndex, collectionUpdated) editGraphqlCollection(props.editingCollectionIndex, collectionUpdated)
@@ -78,7 +86,7 @@ const saveCollection = () => {
} }
const hideModal = () => { const hideModal = () => {
editingName.value = null name.value = null
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -6,13 +6,21 @@
@close="$emit('hide-modal')" @close="$emit('hide-modal')"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="name" <input
placeholder=" " id="selectLabelGqlEditFolder"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
@submit="editFolder" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="editFolder"
/>
<label for="selectLabelGqlEditFolder">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">

View File

@@ -6,13 +6,21 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<HoppSmartInput <div class="flex flex-col">
v-model="requestUpdateData.name" <input
placeholder=" " id="selectLabelGqlEditReq"
:label="t('action.label')" v-model="requestUpdateData.name"
input-styles="floating-input" v-focus
@submit="saveRequest" class="input floating-input"
/> placeholder=" "
type="text"
autocomplete="off"
@keyup.enter="saveRequest"
/>
<label for="selectLabelGqlEditReq">
{{ t("action.label") }}
</label>
</div>
</template> </template>
<template #footer> <template #footer>
<span class="flex space-x-2"> <span class="flex space-x-2">

View File

@@ -34,12 +34,7 @@
:icon="IconFilePlus" :icon="IconFilePlus"
:title="t('request.new')" :title="t('request.new')"
class="hidden group-hover:inline-flex" class="hidden group-hover:inline-flex"
@click=" @click="emit('add-request', { path: folderPath })"
emit('add-request', {
path: folderPath,
index: folder.requests.length,
})
"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -203,7 +198,6 @@ 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 { getTabsRefTo } from "~/helpers/graphql/tab"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
@@ -255,8 +249,10 @@ const collectionIcon = computed(() => {
const pick = () => { const pick = () => {
emit("select", { emit("select", {
pickedType: "gql-my-folder", picked: {
folderPath: props.folderPath, pickedType: "gql-my-folder",
folderPath: props.folderPath,
},
}) })
} }
@@ -277,22 +273,6 @@ const removeFolder = () => {
emit("select", { picked: null }) emit("select", { picked: null })
} }
const possibleTabs = getTabsRefTo((tab) => {
const ctx = tab.document.saveContext
if (!ctx) return false
return (
ctx.originLocation === "user-collection" &&
ctx.folderPath.startsWith(props.folderPath)
)
})
for (const tab of possibleTabs) {
tab.value.document.saveContext = undefined
tab.value.document.isDirty = true
}
removeGraphqlFolder(props.folderPath, props.folder.id) removeGraphqlFolder(props.folderPath, props.folder.id)
toast.success(t("state.deleted")) toast.success(t("state.deleted"))
} }

View File

@@ -20,28 +20,22 @@
/> />
</span> </span>
<span <span
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark" class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="selectRequest()" @click="selectRequest()"
> >
<span class="truncate" :class="{ 'text-accent': isSelected }"> <span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }} {{ request.name }}
</span> </span>
<span
v-if="isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
>
</span>
<span
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
></span>
</span>
</span> </span>
<div class="flex"> <div class="flex">
<HoppButtonSecondary
v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }"
:icon="IconRotateCCW"
:title="t('action.restore')"
class="hidden group-hover:inline-flex"
@click="selectRequest()"
/>
<span> <span>
<tippy <tippy
ref="options" ref="options"
@@ -127,6 +121,7 @@
<script setup lang="ts"> <script setup lang="ts">
import IconCheckCircle from "~icons/lucide/check-circle" import IconCheckCircle from "~icons/lucide/check-circle"
import IconFile from "~icons/lucide/file" import IconFile from "~icons/lucide/file"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconMoreVertical from "~icons/lucide/more-vertical" import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit" import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
@@ -137,12 +132,7 @@ 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 { import { setGQLSession } from "~/newstore/GQLSession"
createNewTab,
getTabRefWithSaveContext,
currentTabID,
currentActiveTab,
} from "~/helpers/graphql/tab"
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
@@ -164,18 +154,6 @@ const props = defineProps({
requestIndex: { type: Number, default: null }, requestIndex: { type: Number, default: null },
}) })
const isActive = computed(() => {
const saveCtx = currentActiveTab.value?.document.saveContext
if (!saveCtx) return false
return (
saveCtx.originLocation === "user-collection" &&
saveCtx.folderPath === props.folderPath &&
saveCtx.requestIndex === props.requestIndex
)
})
// TODO: Better types please // TODO: Better types please
const emit = defineEmits(["select", "edit-request", "duplicate-request"]) const emit = defineEmits(["select", "edit-request", "duplicate-request"])
@@ -201,24 +179,7 @@ const selectRequest = () => {
if (props.saveRequest) { if (props.saveRequest) {
pick() pick()
} else { } else {
const possibleTab = getTabRefWithSaveContext({ setGQLSession({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
})
// Switch to that request if that request is open
if (possibleTab) {
currentTabID.value = possibleTab.value.id
return
}
createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
},
request: cloneDeep( request: cloneDeep(
makeGQLRequest({ makeGQLRequest({
name: props.request.name, name: props.request.name,
@@ -229,7 +190,8 @@ const selectRequest = () => {
auth: props.request.auth, auth: props.request.auth,
}) })
), ),
isDirty: false, schema: "",
response: "",
}) })
} }
} }
@@ -252,18 +214,6 @@ const removeRequest = () => {
emit("select", null) emit("select", null)
} }
// Detach the request from any of the tabs
const possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
})
if (possibleTab) {
possibleTab.value.document.saveContext = undefined
possibleTab.value.document.isDirty = true
}
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id) removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
toast.success(`${t("state.deleted")}`) toast.success(`${t("state.deleted")}`)
} }

View File

@@ -11,7 +11,7 @@
type="search" type="search"
autocomplete="off" autocomplete="off"
:placeholder="t('action.search')" :placeholder="t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent !border-0" class="py-2 pl-4 pr-2 bg-transparent"
/> />
<div <div
class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight" class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"
@@ -137,6 +137,7 @@ import {
addGraphqlFolder, addGraphqlFolder,
saveGraphqlRequestAs, saveGraphqlRequestAs,
} from "~/newstore/collections" } from "~/newstore/collections"
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
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"
@@ -145,7 +146,6 @@ 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 { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -265,22 +265,17 @@ export default defineComponent({
this.$data.editingCollectionIndex = collectionIndex this.$data.editingCollectionIndex = collectionIndex
this.displayModalEdit(true) this.displayModalEdit(true)
}, },
onAddRequest({ name, path, index }) { onAddRequest({ name, path }) {
const newRequest = { const newRequest = {
...currentActiveTab.value.document.request, ...getGQLSession().request,
name, name,
} }
saveGraphqlRequestAs(path, newRequest) saveGraphqlRequestAs(path, newRequest)
setGQLSession({
createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: path,
requestIndex: index,
},
request: newRequest, request: newRequest,
isDirty: false, schema: "",
response: "",
}) })
platform.analytics?.logEvent({ platform.analytics?.logEvent({

View File

@@ -18,13 +18,12 @@
" "
> >
<WorkspaceCurrent :section="t('tab.collections')" /> <WorkspaceCurrent :section="t('tab.collections')" />
<input
<HoppSmartInput
v-model="filterTexts" v-model="filterTexts"
:placeholder="t('action.search')"
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
type="search" type="search"
:autofocus="false" autocomplete="off"
:placeholder="t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent"
:disabled="collectionsType.type === 'team-collections'" :disabled="collectionsType.type === 'team-collections'"
/> />
</div> </div>
@@ -239,7 +238,6 @@ import {
resetTeamRequestsContext, resetTeamRequestsContext,
} from "~/helpers/collection/collection" } from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering" import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler } from "~/helpers/actions"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -2068,8 +2066,4 @@ const getErrorMessage = (err: GQLError<string>) => {
} }
} }
} }
defineActionHandler("collection.new", () => {
displayModalAdd(true)
})
</script> </script>

View File

@@ -11,7 +11,7 @@
t("environment.name") t("environment.name")
}}</label> }}</label>
<input <input
v-model="editingName" v-model="name"
type="text" type="text"
:placeholder="t('environment.variable')" :placeholder="t('environment.variable')"
class="input" class="input"
@@ -21,12 +21,7 @@
<label for="value" class="font-semibold min-w-10">{{ <label for="value" class="font-semibold min-w-10">{{
t("environment.value") t("environment.value")
}}</label> }}</label>
<input <input type="text" :value="value" class="input" />
v-model="editingValue"
type="text"
class="input"
:placeholder="t('environment.value')"
/>
</div> </div>
<div class="flex items-center space-x-8 ml-2"> <div class="flex items-center space-x-8 ml-2">
<label for="scope" class="font-semibold min-w-10"> <label for="scope" class="font-semibold min-w-10">
@@ -93,6 +88,7 @@ const props = defineProps<{
position: { top: number; left: number } position: { top: number; left: number }
name: string name: string
value: string value: string
replaceWithVariable: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -110,12 +106,9 @@ watch(
scope.value = { scope.value = {
type: "global", type: "global",
} }
name.value = ""
replaceWithVariable.value = false replaceWithVariable.value = false
editingName.value = ""
editingValue.value = ""
} }
editingName.value = props.name
editingValue.value = props.value
} }
) )
@@ -139,32 +132,31 @@ const scope = ref<Scope>({
const replaceWithVariable = ref(false) const replaceWithVariable = ref(false)
const editingName = ref(props.name) const name = ref("")
const editingValue = ref(props.value)
const addEnvironment = async () => { const addEnvironment = async () => {
if (!editingName.value) { if (!name.value) {
toast.error(`${t("environment.invalid_name")}`) toast.error(`${t("environment.invalid_name")}`)
return return
} }
if (scope.value.type === "global") { if (scope.value.type === "global") {
addGlobalEnvVariable({ addGlobalEnvVariable({
key: editingName.value, key: name.value,
value: editingValue.value, value: props.value,
}) })
toast.success(`${t("environment.updated")}`) toast.success(`${t("environment.updated")}`)
} else if (scope.value.type === "my-environment") { } else if (scope.value.type === "my-environment") {
addEnvironmentVariable(scope.value.index, { addEnvironmentVariable(scope.value.index, {
key: editingName.value, key: name.value,
value: editingValue.value, value: props.value,
}) })
toast.success(`${t("environment.updated")}`) toast.success(`${t("environment.updated")}`)
} else { } else {
const newVariables = [ const newVariables = [
...scope.value.environment.environment.variables, ...scope.value.environment.environment.variables,
{ {
key: editingName.value, key: name.value,
value: editingValue.value, value: props.value,
}, },
] ]
await pipe( await pipe(
@@ -187,11 +179,11 @@ const addEnvironment = async () => {
} }
if (replaceWithVariable.value) { if (replaceWithVariable.value) {
//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 = `<<${name.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
currentActiveTab.value.document.request.endpoint = currentActiveTab.value.document.request.endpoint =
currentActiveTab.value.document.request.endpoint.replace( currentActiveTab.value.document.request.endpoint.replace(
editingValue.value, props.value,
variableName variableName
) )
} }

View File

@@ -1,302 +1,160 @@
<template> <template>
<div class="flex divide-x divide-dividerLight"> <tippy
<tippy interactive
interactive trigger="click"
trigger="click" theme="popover"
theme="popover" :on-shown="() => tippyActions!.focus()"
:on-shown="() => envSelectorActions!.focus()" >
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="bg-transparent select-wrapper"
> >
<span <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" :icon="IconLayers"
:title="`${t('environment.select')}`" :label="
class="select-wrapper" mdAndLarger
? selectedEnv.type !== 'NO_ENV_SELECTED'
? selectedEnv.name
: `${t('environment.select')}`
: ''
"
class="flex-1 !justify-start pr-8 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
role="menu"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
> >
<HoppButtonSecondary <HoppSmartItem
:icon="IconLayers" v-if="!isScopeSelector"
:label=" :label="`${t('environment.no_environment')}`"
mdAndLarger :info-icon="
? selectedEnv.type !== 'NO_ENV_SELECTED' selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
? selectedEnv.name ? IconCheck
: `${t('environment.select')}` : undefined
: '' "
:active-info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
"
@click="
() => {
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
hide()
}
" "
class="flex-1 !justify-start pr-8 rounded-none"
/> />
</span> <HoppSmartItem
<template #content="{ hide }"> v-else-if="isScopeSelector && modelValue"
<div :label="t('environment.global')"
ref="envSelectorActions" :icon="IconGlobe"
role="menu" :info-icon="modelValue.type === 'global' ? IconCheck : undefined"
class="flex flex-col focus:outline-none" :active-info-icon="modelValue.type === 'global'"
tabindex="0" @click="
@keyup.escape="hide()" () => {
$emit('update:modelValue', {
type: 'global',
})
hide()
}
"
/>
<HoppSmartTabs
v-model="selectedEnvTab"
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary"
render-inactive-tabs
> >
<HoppSmartItem <HoppSmartTab
v-if="!isScopeSelector" :id="'my-environments'"
:label="`${t('environment.no_environment')}`" :label="`${t('environment.my_environments')}`"
:info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
? IconCheck
: undefined
"
:active-info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
"
@click="
() => {
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
hide()
}
"
/>
<HoppSmartItem
v-else-if="isScopeSelector && modelValue"
:label="t('environment.global')"
:icon="IconGlobe"
:info-icon="modelValue.type === 'global' ? IconCheck : undefined"
:active-info-icon="modelValue.type === 'global'"
@click="
() => {
$emit('update:modelValue', {
type: 'global',
})
hide()
}
"
/>
<HoppSmartTabs
v-model="selectedEnvTab"
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
!isTeamSelected || workspace.type === 'personal'
? 'bg-primaryLight'
: ''
}`"
render-inactive-tabs
> >
<HoppSmartTab <HoppSmartItem
:id="'my-environments'" v-for="(gen, index) in myEnvironments"
:label="`${t('environment.my_environments')}`" :key="`gen-${index}`"
:icon="IconLayers"
:label="gen.name"
:info-icon="isEnvActive(index) ? IconCheck : undefined"
:active-info-icon="isEnvActive(index)"
@click="
() => {
handleEnvironmentChange(index, {
type: 'my-environment',
environment: gen,
})
hide()
}
"
/>
<HoppSmartPlaceholder
v-if="myEnvironments.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
> >
</HoppSmartPlaceholder>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
>
<div
v-if="teamListLoading"
class="flex flex-col items-center justify-center p-4"
>
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-else-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in myEnvironments" v-for="(gen, index) in teamEnvironmentList"
:key="`gen-${index}`" :key="`gen-team-${index}`"
:icon="IconLayers" :icon="IconLayers"
:label="gen.name" :label="gen.environment.name"
:info-icon="isEnvActive(index) ? IconCheck : undefined" :info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
:active-info-icon="isEnvActive(index)" :active-info-icon="isEnvActive(gen.id)"
@click=" @click="
() => { () => {
handleEnvironmentChange(index, { handleEnvironmentChange(index, {
type: 'my-environment', type: 'team-environment',
environment: gen, environment: gen,
}) })
hide() hide()
} }
" "
/> />
<div
v-if="myEnvironments.length === 0" <HoppSmartPlaceholder
class="flex flex-col items-center justify-center text-secondaryLight" v-if="teamEnvironmentList.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
> >
<img </HoppSmartPlaceholder>
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
>
<div
v-if="teamListLoading"
class="flex flex-col items-center justify-center p-4"
>
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">
{{ t("state.loading") }}
</span>
</div>
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem
v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name"
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
:active-info-icon="isEnvActive(gen.id)"
@click="
() => {
handleEnvironmentChange(index, {
type: 'team-environment',
environment: gen,
})
hide()
}
"
/>
<div
v-if="teamEnvironmentList.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</div>
<div
v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div>
</template>
</tippy>
<span class="flex">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => envQuickPeekActions!.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.quick_peek')}`"
:icon="IconEye"
class="!px-4"
/>
<template #content="{ hide }">
<div
ref="envQuickPeekActions"
role="menu"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<div
class="sticky top-0 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
>
{{ t("environment.global_variables") }}
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.edit')"
:icon="IconEdit"
@click="
() => {
editGlobalEnv()
hide()
}
"
/>
</div>
<div class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4">
<span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span class="w-full min-w-32 truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
<div
v-for="(variable, index) in globalEnvs"
:key="index"
class="flex flex-1 space-x-4"
>
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
{{ variable.key }}
</span>
<span class="text-secondaryLight w-full min-w-32 truncate">
{{ variable.value }}
</span>
</div>
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
{{ t("environment.empty_variables") }}
</div>
</div> </div>
<div <div
class="sticky top-0 mt-2 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4" v-if="!teamListLoading && teamAdapterError"
:class="{ class="flex flex-col items-center py-4"
'bg-primaryLight': !selectedEnv.variables,
}"
> >
{{ t("environment.list") }} <icon-lucide-help-circle class="mb-4 svg-icons" />
<HoppButtonSecondary {{ getErrorMessage(teamAdapterError) }}
v-tippy="{ theme: 'tooltip' }"
:disabled="!selectedEnv.variables"
:title="t('action.edit')"
:icon="IconEdit"
@click="
() => {
editEnv()
hide()
}
"
/>
</div> </div>
<div </HoppSmartTab>
v-if="selectedEnv.type === 'NO_ENV_SELECTED'" </HoppSmartTabs>
class="text-secondaryLight my-2 flex flex-col flex-1 pl-4" </div>
> </template>
{{ t("environment.no_active_environment") }} </tippy>
</div>
<div v-else class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4">
<span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span class="w-full min-w-32 truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
<div
v-for="(variable, index) in environmentVariables"
:key="index"
class="flex flex-1 space-x-4"
>
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
{{ variable.key }}
</span>
<span class="text-secondaryLight w-full min-w-32 truncate">
{{ variable.value }}
</span>
</div>
<div
v-if="environmentVariables.length === 0"
class="text-secondaryLight"
>
{{ t("environment.empty_variables") }}
</div>
</div>
</div>
</template>
</tippy>
</span>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from "vue" import { computed, onMounted, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers" import IconLayers from "~icons/lucide/layers"
import IconEye from "~icons/lucide/eye"
import IconEdit from "~icons/lucide/edit"
import IconGlobe from "~icons/lucide/globe" import IconGlobe from "~icons/lucide/globe"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
@@ -304,7 +162,6 @@ import { GQLError } from "~/helpers/backend/GQLClient"
import { useReadonlyStream, useStream } from "~/composables/stream" import { useReadonlyStream, useStream } from "~/composables/stream"
import { import {
environments$, environments$,
globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
@@ -312,14 +169,12 @@ import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core" import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { invokeAction } from "~/helpers/actions"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { Environment } from "@hoppscotch/data"
import { onMounted } from "vue"
import { onLoggedIn } from "~/composables/auth"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter" import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate" import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { Environment } from "@hoppscotch/data"
type Scope = type Scope =
| { | {
@@ -334,10 +189,12 @@ type Scope =
type: "team-environment" type: "team-environment"
environment: TeamEnvironment environment: TeamEnvironment
} }
const props = defineProps<{ const props = defineProps<{
isScopeSelector?: boolean isScopeSelector?: boolean
modelValue?: Scope modelValue?: Scope
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", data: Scope): void (e: "update:modelValue", data: Scope): void
}>() }>()
@@ -373,6 +230,7 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
type: "team", type: "team",
}) })
} }
watch( watch(
() => myTeams.value, () => myTeams.value,
(newTeams) => { (newTeams) => {
@@ -395,6 +253,32 @@ const teamEnvironmentList = useReadonlyStream(
[] []
) )
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
setSelectedEnvironmentIndex
)
const isTeamSelected = computed(
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
)
const selectedEnvTab = ref<EnvironmentType>("my-environments")
watch(
() => workspace.value,
(newVal) => {
if (newVal.type === "personal") {
selectedEnvTab.value = "my-environments"
} else {
selectedEnvTab.value = "team-environments"
if (newVal.teamID) {
teamEnvListAdapter.changeTeamID(newVal.teamID)
}
}
}
)
const handleEnvironmentChange = ( const handleEnvironmentChange = (
index: number, index: number,
env?: env?:
@@ -436,6 +320,7 @@ const handleEnvironmentChange = (
} }
} }
} }
const isEnvActive = (id: string | number) => { const isEnvActive = (id: string | number) => {
if (props.isScopeSelector) { if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") { if (props.modelValue?.type === "my-environment") {
@@ -459,32 +344,6 @@ const isEnvActive = (id: string | number) => {
} }
} }
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
setSelectedEnvironmentIndex
)
const isTeamSelected = computed(
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
)
const selectedEnvTab = ref<EnvironmentType>("my-environments")
watch(
() => workspace.value,
(newVal) => {
if (newVal.type === "personal") {
selectedEnvTab.value = "my-environments"
} else {
selectedEnvTab.value = "team-environments"
if (newVal.teamID) {
teamEnvListAdapter.changeTeamID(newVal.teamID)
}
}
}
)
const selectedEnv = computed(() => { const selectedEnv = computed(() => {
if (props.isScopeSelector) { if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") { if (props.modelValue?.type === "my-environment") {
@@ -504,13 +363,10 @@ const selectedEnv = computed(() => {
} }
} else { } else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") { if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
myEnvironments.value[selectedEnvironmentIndex.value.index]
return { return {
type: "MY_ENV", type: "MY_ENV",
index: selectedEnvironmentIndex.value.index, index: selectedEnvironmentIndex.value.index,
name: environment.name, name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
variables: environment.variables,
} }
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") { } else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find( const teamEnv = teamEnvironmentList.value.find(
@@ -524,7 +380,6 @@ const selectedEnv = computed(() => {
type: "TEAM_ENV", type: "TEAM_ENV",
name: teamEnv.environment.name, name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID, teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
} }
} else { } else {
return { type: "NO_ENV_SELECTED" } return { type: "NO_ENV_SELECTED" }
@@ -574,8 +429,7 @@ onMounted(() => {
}) })
// Template refs // Template refs
const envSelectorActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const envQuickPeekActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") { if (err.type === "network_error") {
@@ -589,32 +443,4 @@ const getErrorMessage = (err: GQLError<string>) => {
} }
} }
} }
const globalEnvs = useReadonlyStream(globalEnv$, [])
const environmentVariables = computed(() => {
if (selectedEnv.value.variables) {
return selectedEnv.value.variables
} else {
return []
}
})
const editGlobalEnv = () => {
invokeAction("modals.my.environment.edit", {
envName: "Global",
})
}
const editEnv = () => {
if (selectedEnv.value.type === "MY_ENV" && selectedEnv.value.name) {
invokeAction("modals.my.environment.edit", {
envName: selectedEnv.value.name,
})
} else if (selectedEnv.value.type === "TEAM_ENV" && selectedEnv.value.name) {
invokeAction("modals.team.environment.edit", {
envName: selectedEnv.value.name,
})
}
}
</script> </script>

View File

@@ -11,9 +11,9 @@
@edit-environment="editEnvironment('Global')" @edit-environment="editEnvironment('Global')"
/> />
</div> </div>
<EnvironmentsMy v-show="environmentType.type === 'my-environments'" /> <EnvironmentsMy v-if="environmentType.type === 'my-environments'" />
<EnvironmentsTeams <EnvironmentsTeams
v-show="environmentType.type === 'team-environments'" v-if="environmentType.type === 'team-environments'"
:team="environmentType.selectedTeam" :team="environmentType.selectedTeam"
:team-environments="teamEnvironmentList" :team-environments="teamEnvironmentList"
:loading="loading" :loading="loading"
@@ -34,13 +34,6 @@
@hide-modal="displayModalNew(false)" @hide-modal="displayModalNew(false)"
/> />
</div> </div>
<HoppSmartConfirmModal
:show="showConfirmRemoveEnvModal"
:title="t('confirm.remove_team')"
@hide-modal="showConfirmRemoveEnvModal = false"
@resolve="removeSelectedEnvironment()"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -51,7 +44,6 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useReadonlyStream, useStream } from "@composables/stream" import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { import {
getSelectedEnvironmentIndex,
globalEnv$, globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
@@ -62,15 +54,8 @@ import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter" import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate" import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth" import { onLoggedIn } from "~/composables/auth"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { GQLError } from "~/helpers/backend/GQLClient"
import { deleteEnvironment } from "~/newstore/environments"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { useToast } from "~/composables/toast"
const t = useI18n() const t = useI18n()
const toast = useToast()
type EnvironmentType = "my-environments" | "team-environments" type EnvironmentType = "my-environments" | "team-environments"
@@ -183,7 +168,6 @@ watch(
} }
) )
const showConfirmRemoveEnvModal = ref(false)
const showModalNew = ref(false) const showModalNew = ref(false)
const showModalDetails = ref(false) const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit") const action = ref<"new" | "edit">("edit")
@@ -210,47 +194,14 @@ const editEnvironment = (environmentIndex: "Global") => {
displayModalEdit(true) displayModalEdit(true)
} }
const removeSelectedEnvironment = () => {
const selectedEnvIndex = getSelectedEnvironmentIndex()
if (selectedEnvIndex?.type === "NO_ENV_SELECTED") return
if (selectedEnvIndex?.type === "MY_ENV") {
deleteEnvironment(selectedEnvIndex.index)
toast.success(`${t("state.deleted")}`)
}
if (selectedEnvIndex?.type === "TEAM_ENV") {
pipe(
deleteTeamEnvironment(selectedEnvIndex.teamEnvID),
TE.match(
(err: GQLError<string>) => {
console.error(err)
},
() => {
toast.success(`${t("team_environment.deleted")}`)
}
)
)()
}
}
const resetSelectedData = () => { const resetSelectedData = () => {
editingEnvironmentIndex.value = null editingEnvironmentIndex.value = null
} }
defineActionHandler("modals.environment.new", () => {
action.value = "new"
showModalDetails.value = true
})
defineActionHandler("modals.environment.delete-selected", () => {
showConfirmRemoveEnvModal.value = true
})
defineActionHandler( defineActionHandler(
"modals.my.environment.edit", "modals.my.environment.edit",
({ envName, variableName }) => { ({ envName, variableName }) => {
if (variableName) editingVariableName.value = variableName editingVariableName.value = variableName
envName === "Global" && editEnvironment("Global") envName === "Global" && editEnvironment("Global")
} }
) )
@@ -300,7 +251,7 @@ watch(
defineActionHandler("modals.environment.add", ({ envName, variableName }) => { defineActionHandler("modals.environment.add", ({ envName, variableName }) => {
editingVariableName.value = envName editingVariableName.value = envName
if (variableName) editingVariableValue.value = variableName editingVariableValue.value = variableName
displayModalNew(true) displayModalNew(true)
}) })
</script> </script>

View File

@@ -7,15 +7,22 @@
> >
<template #body> <template #body>
<div class="flex flex-col"> <div class="flex flex-col">
<HoppSmartInput <div class="relative flex">
v-model="editingName" <input
placeholder=" " id="selectLabelEnvEdit"
:label="t('action.label')" v-model="name"
input-styles="floating-input" v-focus
:disabled="editingEnvironmentIndex === 'Global'" class="input floating-input"
@submit="saveEnvironment" placeholder=" "
/> type="text"
autocomplete="off"
:disabled="editingEnvironmentIndex === 'Global'"
@keyup.enter="saveEnvironment"
/>
<label for="selectLabelEnvEdit">
{{ t("action.label") }}
</label>
</div>
<div class="flex items-center justify-between flex-1"> <div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4"> <label for="variableList" class="p-4">
{{ t("environment.variable_list") }} {{ t("environment.variable_list") }}
@@ -81,6 +88,7 @@
<HoppButtonSecondary <HoppButtonSecondary
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
class="mb-4"
@click="addEnvironmentVariable" @click="addEnvironmentVariable"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -170,7 +178,7 @@ const emit = defineEmits<{
const idTicker = ref(0) const idTicker = ref(0)
const editingName = ref<string | null>(null) const name = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([ const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } }, { id: idTicker.value++, env: { key: "", value: "" } },
]) ])
@@ -223,12 +231,10 @@ const liveEnvs = computed(() => {
} }
if (props.editingEnvironmentIndex === "Global") { if (props.editingEnvironmentIndex === "Global") {
return [ return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))]
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
]
} else { } else {
return [ return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })), ...vars.value.map((x) => ({ ...x.env, source: name.value! })),
...globalVars.value.map((x) => ({ ...x, source: "Global" })), ...globalVars.value.map((x) => ({ ...x, source: "Global" })),
] ]
} }
@@ -238,7 +244,7 @@ watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
editingName.value = workingEnv.value?.name ?? null name.value = workingEnv.value?.name ?? null
vars.value = pipe( vars.value = pipe(
workingEnv.value?.variables ?? [], workingEnv.value?.variables ?? [],
A.map((e) => ({ A.map((e) => ({
@@ -271,7 +277,7 @@ const removeEnvironmentVariable = (index: number) => {
} }
const saveEnvironment = () => { const saveEnvironment = () => {
if (!editingName.value) { if (!name.value) {
toast.error(`${t("environment.invalid_name")}`) toast.error(`${t("environment.invalid_name")}`)
return return
} }
@@ -287,13 +293,13 @@ const saveEnvironment = () => {
) )
const environmentUpdated: Environment = { const environmentUpdated: Environment = {
name: editingName.value, name: name.value,
variables: filterdVariables, variables: filterdVariables,
} }
if (props.action === "new") { if (props.action === "new") {
// Creating a new environment // Creating a new environment
createEnvironment(editingName.value, environmentUpdated.variables) createEnvironment(name.value, environmentUpdated.variables)
setSelectedEnvironmentIndex({ setSelectedEnvironmentIndex({
type: "MY_ENV", type: "MY_ENV",
index: envList.value.length - 1, index: envList.value.length - 1,
@@ -331,7 +337,7 @@ const saveEnvironment = () => {
} }
const hideModal = () => { const hideModal = () => {
editingName.value = null name.value = null
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -158,7 +158,5 @@ const duplicateEnvironments = () => {
cloneDeep(getGlobalVariables()) cloneDeep(getGlobalVariables())
) )
} else duplicateEnvironment(props.environmentIndex) } else duplicateEnvironment(props.environmentIndex)
toast.success(`${t("environment.duplicated")}`)
} }
</script> </script>

View File

@@ -42,6 +42,7 @@
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
outline outline
class="mb-4"
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -108,7 +109,7 @@ const resetSelectedData = () => {
defineActionHandler( defineActionHandler(
"modals.my.environment.edit", "modals.my.environment.edit",
({ envName, variableName }) => { ({ envName, variableName }) => {
if (variableName) editingVariableName.value = variableName editingVariableName.value = variableName
const envIndex: number = environments.value.findIndex( const envIndex: number = environments.value.findIndex(
(environment: Environment) => { (environment: Environment) => {
return environment.name === envName return environment.name === envName

View File

@@ -7,15 +7,23 @@
> >
<template #body> <template #body>
<div class="flex flex-col px-2"> <div class="flex flex-col px-2">
<HoppSmartInput <div class="relative flex">
v-model="editingName" <input
placeholder=" " id="selectLabelEnvEdit"
:input-styles="['floating-input', isViewer && 'opacity-25']" v-model="name"
:label="t('action.label')" v-focus
:disabled="isViewer" class="input floating-input"
@submit="saveEnvironment" :class="isViewer && 'opacity-25'"
/> placeholder=""
type="text"
autocomplete="off"
:disabled="isViewer"
@keyup.enter="saveEnvironment"
/>
<label for="selectLabelEnvEdit">
{{ t("action.label") }}
</label>
</div>
<div class="flex items-center justify-between flex-1"> <div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4"> <label for="variableList" class="p-4">
{{ t("environment.variable_list") }} {{ t("environment.variable_list") }}
@@ -86,11 +94,13 @@
disabled disabled
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
class="mb-4"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
class="mb-4"
@click="addEnvironmentVariable" @click="addEnvironmentVariable"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -180,7 +190,7 @@ const emit = defineEmits<{
const idTicker = ref(0) const idTicker = ref(0)
const editingName = ref<string | null>(null) const name = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([ const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } }, { id: idTicker.value++, env: { key: "", value: "" } },
]) ])
@@ -206,9 +216,7 @@ const liveEnvs = computed(() => {
if (evnExpandError.value) { if (evnExpandError.value) {
return [] return []
} else { } else {
return [ return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))]
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
]
} }
}) })
@@ -217,7 +225,7 @@ watch(
(show) => { (show) => {
if (show) { if (show) {
if (props.action === "new") { if (props.action === "new") {
editingName.value = null name.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 }) => ({
@@ -226,7 +234,7 @@ watch(
})) }))
) )
} else if (props.editingEnvironment !== null) { } else if (props.editingEnvironment !== null) {
editingName.value = props.editingEnvironment.environment.name ?? null name.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 }) => ({
@@ -264,7 +272,7 @@ const isLoading = ref(false)
const saveEnvironment = async () => { const saveEnvironment = async () => {
isLoading.value = true isLoading.value = true
if (!editingName.value) { if (!name.value) {
toast.error(`${t("environment.invalid_name")}`) toast.error(`${t("environment.invalid_name")}`)
return return
} }
@@ -289,7 +297,7 @@ const saveEnvironment = async () => {
createTeamEnvironment( createTeamEnvironment(
JSON.stringify(filterdVariables), JSON.stringify(filterdVariables),
props.editingTeamId, props.editingTeamId,
editingName.value name.value
), ),
TE.match( TE.match(
(err: GQLError<string>) => { (err: GQLError<string>) => {
@@ -312,7 +320,7 @@ const saveEnvironment = async () => {
updateTeamEnvironment( updateTeamEnvironment(
JSON.stringify(filterdVariables), JSON.stringify(filterdVariables),
props.editingEnvironment.id, props.editingEnvironment.id,
editingName.value name.value
), ),
TE.match( TE.match(
(err: GQLError<string>) => { (err: GQLError<string>) => {
@@ -331,7 +339,7 @@ const saveEnvironment = async () => {
} }
const hideModal = () => { const hideModal = () => {
editingName.value = null name.value = null
emit("hide-modal") emit("hide-modal")
} }

View File

@@ -154,7 +154,7 @@ const duplicateEnvironments = () => {
toast.error(`${getErrorMessage(err)}`) toast.error(`${getErrorMessage(err)}`)
}, },
() => { () => {
toast.success(`${t("environment.duplicated")}`) toast.success(`${t("team_environment.duplicate")}`)
} }
) )
)() )()

View File

@@ -54,6 +54,7 @@
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
disabled disabled
filled filled
class="mb-4"
:icon="IconPlus" :icon="IconPlus"
:title="t('team.no_access')" :title="t('team.no_access')"
:label="t('action.new')" :label="t('action.new')"
@@ -63,6 +64,7 @@
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
outline outline
class="mb-4"
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -176,7 +178,7 @@ const getErrorMessage = (err: GQLError<string>) => {
defineActionHandler( defineActionHandler(
"modals.team.environment.edit", "modals.team.environment.edit",
({ envName, variableName }) => { ({ envName, variableName }) => {
if (variableName) editingVariableName.value = variableName editingVariableName.value = variableName
const teamEnvToEdit = props.teamEnvironments.find( const teamEnvToEdit = props.teamEnvironments.find(
(environment) => environment.environment.name === envName (environment) => environment.environment.name === envName
) )

View File

@@ -9,22 +9,27 @@
<template #body> <template #body>
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2"> <div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
<HoppSmartItem <HoppSmartItem
v-for="provider in allowedAuthProviders" :loading="signingInWithGitHub"
:key="provider.id" :icon="IconGithub"
:loading="provider.isLoading.value" :label="`${t('auth.continue_with_github')}`"
:icon="provider.icon" @click="signInWithGithub"
:label="provider.label"
@click="provider.action"
/> />
<hr v-if="additonalLoginItems.length > 0" />
<HoppSmartItem <HoppSmartItem
v-for="loginItem in additonalLoginItems" :loading="signingInWithGoogle"
:key="loginItem.id" :icon="IconGoogle"
:icon="loginItem.icon" :label="`${t('auth.continue_with_google')}`"
:label="loginItem.text(t)" @click="signInWithGoogle"
@click="doAdditionalLoginItemClickAction(loginItem)" />
<HoppSmartItem
:loading="signingInWithMicrosoft"
:icon="IconMicrosoft"
:label="`${t('auth.continue_with_microsoft')}`"
@click="signInWithMicrosoft"
/>
<HoppSmartItem
:icon="IconEmail"
:label="`${t('auth.continue_with_email')}`"
@click="mode = 'email'"
/> />
</div> </div>
<form <form
@@ -32,14 +37,24 @@
class="flex flex-col space-y-2" class="flex flex-col space-y-2"
@submit.prevent="signInWithEmail" @submit.prevent="signInWithEmail"
> >
<HoppSmartInput <div class="flex flex-col">
v-model="form.email" <input
type="email" id="email"
placeholder=" " v-model="form.email"
:label="t('auth.email')" v-focus
input-styles="floating-input" class="input floating-input"
/> placeholder=" "
type="email"
name="email"
autocomplete="off"
required
spellcheck="false"
autofocus
/>
<label for="email">
{{ t("auth.email") }}
</label>
</div>
<HoppButtonPrimary <HoppButtonPrimary
:loading="signingInWithEmail" :loading="signingInWithEmail"
type="submit" type="submit"
@@ -108,138 +123,124 @@
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script lang="ts">
import { Ref, computed, onMounted, ref } from "vue" import { defineComponent } from "vue"
import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { platform } from "~/platform" import { platform } from "~/platform"
import { setLocalConfig } from "~/newstore/localpersistence"
import IconGithub from "~icons/auth/github" import IconGithub from "~icons/auth/github"
import IconGoogle from "~icons/auth/google" import IconGoogle from "~icons/auth/google"
import IconEmail from "~icons/auth/email" import IconEmail from "~icons/auth/email"
import IconMicrosoft from "~icons/auth/microsoft" import IconMicrosoft from "~icons/auth/microsoft"
import IconArrowLeft from "~icons/lucide/arrow-left" import IconArrowLeft from "~icons/lucide/arrow-left"
import { setLocalConfig } from "~/newstore/localpersistence"
import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { LoginItemDef } from "~/platform/auth" export default defineComponent({
props: {
show: Boolean,
},
emits: ["hide-modal"],
setup() {
const { subscribeToStream } = useStreamSubscriber()
defineProps<{ const tosLink = import.meta.env.VITE_APP_TOS_LINK
show: boolean const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
}>()
const emit = defineEmits<{ return {
(e: "hide-modal"): void subscribeToStream,
}>() t: useI18n(),
toast: useToast(),
IconGithub,
IconGoogle,
IconEmail,
IconMicrosoft,
IconArrowLeft,
tosLink,
privacyPolicyLink,
}
},
data() {
return {
form: {
email: "",
},
signingInWithGoogle: false,
signingInWithGitHub: false,
signingInWithMicrosoft: false,
signingInWithEmail: false,
mode: "sign-in",
}
},
mounted() {
const currentUser$ = platform.auth.getCurrentUserStream()
const { subscribeToStream } = useStreamSubscriber() this.subscribeToStream(currentUser$, (user) => {
const t = useI18n() if (user) this.hideModal()
const toast = useToast() })
},
methods: {
showLoginSuccess() {
this.toast.success(`${this.t("auth.login_success")}`)
},
async signInWithGoogle() {
this.signingInWithGoogle = true
const form = { try {
email: "", await platform.auth.signInUserWithGoogle()
} } catch (e) {
console.error(e)
const signingInWithGoogle = ref(false) /*
const signingInWithGitHub = ref(false)
const signingInWithMicrosoft = ref(false)
const signingInWithEmail = ref(false)
const mode = ref("sign-in")
const tosLink = import.meta.env.VITE_APP_TOS_LINK
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
type AuthProviderItem = {
id: string
icon: typeof IconGithub
label: string
action: (...args: any[]) => any
isLoading: Ref<boolean>
}
const additonalLoginItems = computed(
() => platform.auth.additionalLoginItems ?? []
)
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
await item.onClick()
emit("hide-modal")
}
onMounted(() => {
const currentUser$ = platform.auth.getCurrentUserStream()
subscribeToStream(currentUser$, (user) => {
if (user) hideModal()
})
})
const showLoginSuccess = () => {
toast.success(`${t("auth.login_success")}`)
}
const signInWithGoogle = async () => {
signingInWithGoogle.value = true
try {
await platform.auth.signInUserWithGoogle()
} catch (e) {
console.error(e)
/*
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25 Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
*/ */
toast.error(`${t("error.something_went_wrong")}`) this.toast.error(`${this.t("error.something_went_wrong")}`)
} }
signingInWithGoogle.value = false this.signingInWithGoogle = false
} },
async signInWithGithub() {
this.signingInWithGitHub = true
const signInWithGithub = async () => { const result = await platform.auth.signInUserWithGithub()
signingInWithGitHub.value = true
const result = await platform.auth.signInUserWithGithub() if (!result) {
this.signingInWithGitHub = false
return
}
if (!result) { if (result.type === "success") {
signingInWithGitHub.value = false // this.showLoginSuccess()
return } else if (result.type === "account-exists-with-different-cred") {
} this.toast.info(`${this.t("auth.account_exists")}`, {
duration: 0,
closeOnSwipe: false,
action: {
text: `${this.t("action.yes")}`,
onClick: async (_, toastObject) => {
await result.link()
this.showLoginSuccess()
if (result.type === "success") { toastObject.goAway(0)
// this.showLoginSuccess() },
} else if (result.type === "account-exists-with-different-cred") { },
toast.info(`${t("auth.account_exists")}`, { })
duration: 0, } else {
closeOnSwipe: false, console.log("error logging into github", result.err)
action: { this.toast.error(`${this.t("error.something_went_wrong")}`)
text: `${t("action.yes")}`, }
onClick: async (_, toastObject) => {
await result.link()
showLoginSuccess()
toastObject.goAway(0) this.signingInWithGitHub = false
}, },
}, async signInWithMicrosoft() {
}) this.signingInWithMicrosoft = true
} else {
console.log("error logging into github", result.err)
toast.error(`${t("error.something_went_wrong")}`)
}
signingInWithGitHub.value = false try {
} await platform.auth.signInUserWithMicrosoft()
// this.showLoginSuccess()
const signInWithMicrosoft = async () => { } catch (e) {
signingInWithMicrosoft.value = true console.error(e)
/*
try {
await platform.auth.signInUserWithMicrosoft()
// this.showLoginSuccess()
} catch (e) {
console.error(e)
/*
A auth/account-exists-with-different-credential Firebase error wont happen between MS with Google or Github A auth/account-exists-with-different-credential Firebase error wont happen between MS with Google or Github
If a Github account exists and user then logs in with MS email we get a "Something went wrong toast" and console errors and MS replaces GH as only provider. If a Github account exists and user then logs in with MS email we get a "Something went wrong toast" and console errors and MS replaces GH as only provider.
The error messages are as follows: The error messages are as follows:
@@ -247,84 +248,34 @@ const signInWithMicrosoft = async () => {
@firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set @firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set
They may be related to https://github.com/firebase/firebaseui-web/issues/947 They may be related to https://github.com/firebase/firebaseui-web/issues/947
*/ */
toast.error(`${t("error.something_went_wrong")}`) this.toast.error(`${this.t("error.something_went_wrong")}`)
} }
signingInWithMicrosoft.value = false this.signingInWithMicrosoft = false
} },
async signInWithEmail() {
const signInWithEmail = async () => { this.signingInWithEmail = true
signingInWithEmail.value = true
await platform.auth
await platform.auth .signInWithEmail(this.form.email)
.signInWithEmail(form.email) .then(() => {
.then(() => { this.mode = "email-sent"
mode.value = "email-sent" setLocalConfig("emailForSignIn", this.form.email)
setLocalConfig("emailForSignIn", form.email) })
}) .catch((e) => {
.catch((e) => { console.error(e)
console.error(e) this.toast.error(e.message)
toast.error(e.message) this.signingInWithEmail = false
signingInWithEmail.value = false })
}) .finally(() => {
.finally(() => { this.signingInWithEmail = false
signingInWithEmail.value = false })
}) },
} hideModal() {
this.mode = "sign-in"
const hideModal = () => { this.toast.clear()
mode.value = "sign-in" this.$emit("hide-modal")
toast.clear()
emit("hide-modal")
}
const authProviders: AuthProviderItem[] = [
{
id: "GITHUB",
icon: IconGithub,
label: t("auth.continue_with_github"),
action: signInWithGithub,
isLoading: signingInWithGitHub,
},
{
id: "GOOGLE",
icon: IconGoogle,
label: t("auth.continue_with_google"),
action: signInWithGoogle,
isLoading: signingInWithGoogle,
},
{
id: "MICROSOFT",
icon: IconMicrosoft,
label: t("auth.continue_with_microsoft"),
action: signInWithMicrosoft,
isLoading: signingInWithMicrosoft,
},
{
id: "EMAIL",
icon: IconEmail,
label: t("auth.continue_with_email"),
action: () => {
mode.value = "email"
}, },
isLoading: signingInWithEmail,
}, },
] })
// Do not format the `import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS` call into multiple lines!
// prettier-ignore
const allowedAuthProvidersIDsString =
import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
? allowedAuthProvidersIDsString.split(",")
: []
const allowedAuthProviders =
allowedAuthProvidersIDs.length > 0
? authProviders.filter((provider) =>
allowedAuthProvidersIDs.includes(provider.id)
)
: authProviders
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<div <div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight" class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
> >
<span class="flex items-center"> <span class="flex items-center">
<label class="font-semibold truncate text-secondaryLight"> <label class="font-semibold truncate text-secondaryLight">
@@ -32,7 +32,7 @@
:active="authName === 'None'" :active="authName === 'None'"
@click=" @click="
() => { () => {
auth.authType = 'none' authType = 'none'
hide() hide()
} }
" "
@@ -43,7 +43,7 @@
:active="authName === 'Basic Auth'" :active="authName === 'Basic Auth'"
@click=" @click="
() => { () => {
auth.authType = 'basic' authType = 'basic'
hide() hide()
} }
" "
@@ -54,7 +54,7 @@
:active="authName === 'Bearer'" :active="authName === 'Bearer'"
@click=" @click="
() => { () => {
auth.authType = 'bearer' authType = 'bearer'
hide() hide()
} }
" "
@@ -65,7 +65,7 @@
:active="authName === 'OAuth 2.0'" :active="authName === 'OAuth 2.0'"
@click=" @click="
() => { () => {
auth.authType = 'oauth-2' authType = 'oauth-2'
hide() hide()
} }
" "
@@ -76,7 +76,7 @@
:active="authName === 'API key'" :active="authName === 'API key'"
@click=" @click="
() => { () => {
auth.authType = 'api-key' authType = 'api-key'
hide() hide()
} }
" "
@@ -90,8 +90,8 @@
:on="!URLExcludes.auth" :on="!URLExcludes.auth"
@change="setExclude('auth', !$event)" @change="setExclude('auth', !$event)"
> >
{{ $t("authorization.include_in_url") }} {{ t("authorization.include_in_url") }}
</HoppSmartCheckbox>--> </HoppSmartCheckbox> -->
<HoppSmartCheckbox <HoppSmartCheckbox
:on="authActive" :on="authActive"
class="px-2" class="px-2"
@@ -115,7 +115,7 @@
</div> </div>
</div> </div>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="auth.authType === 'none'" v-if="authType === 'none'"
:src="`/images/states/${colorMode.value}/login.svg`" :src="`/images/states/${colorMode.value}/login.svg`"
:alt="`${t('empty.authorization')}`" :alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')" :text="t('empty.authorization')"
@@ -127,47 +127,114 @@
blank blank
:icon="IconExternalLink" :icon="IconExternalLink"
reverse reverse
class="mb-4"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight"> <div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight"> <div class="w-2/3 border-r border-dividerLight">
<div v-if="auth.authType === 'basic'"> <div v-if="authType === 'basic'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="auth.username" v-model="basicUsername"
:environment-highlights="false" :environment-highlights="false"
:placeholder="t('authorization.username')" :placeholder="t('authorization.username')"
/> />
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="auth.password" v-model="basicPassword"
:environment-highlights="false" :environment-highlights="false"
:placeholder="t('authorization.password')" :placeholder="t('authorization.password')"
/> />
</div> </div>
</div> </div>
<div v-if="auth.authType === 'bearer'"> <div v-if="authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="auth.token" v-model="bearerToken"
:environment-highlights="false" :environment-highlights="false"
placeholder="Token" placeholder="Token"
/> />
</div> </div>
</div> </div>
<div v-if="auth.authType === 'oauth-2'"> <div v-if="authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="auth.token" v-model="oauth2Token"
:environment-highlights="false" :environment-highlights="false"
placeholder="Token" placeholder="Token"
/> />
</div> </div>
<HttpOAuth2Authorization v-model="auth" /> <HttpOAuth2Authorization />
</div> </div>
<div v-if="auth.authType === 'api-key'"> <div v-if="authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" /> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="apiKey"
:environment-highlights="false"
placeholder="Key"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="apiValue"
:environment-highlights="false"
placeholder="Value"
/>
</div>
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
<label class="ml-4 text-secondaryLight">
{{ t("authorization.pass_key_by") }}
</label>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => authTippyActions.focus()"
>
<span class="select-wrapper">
<HoppButtonSecondary
:label="addTo || t('state.none')"
class="pr-8 ml-2 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="authTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
:active="addTo === 'Headers'"
:label="'Headers'"
@click="
() => {
addTo = 'Headers'
hide()
}
"
/>
<HoppSmartItem
:icon="
addTo === 'Query params' ? IconCircleDot : IconCircle
"
:active="addTo === 'Query params'"
:label="'Query params'"
@click="
() => {
addTo = 'Query params'
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div> </div>
</div> </div>
<div <div
@@ -190,45 +257,55 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconHelpCircle from "~icons/lucide/help-circle" import { computed, ref, Ref } from "vue"
import {
HoppGQLAuthAPIKey,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthOAuth2,
} from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { gqlAuth$, setGQLAuth } from "~/newstore/GQLSession"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconExternalLink from "~icons/lucide/external-link" import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot" import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle" import IconCircle from "~icons/lucide/circle"
import { computed, ref } from "vue"
import { HoppGQLAuth } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const props = defineProps<{ const auth = useStream(
modelValue: HoppGQLAuth gqlAuth$,
}>() { authType: "none", authActive: true },
setGQLAuth
const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLAuth): void
}>()
const auth = useVModel(props, "modelValue", emit)
const AUTH_KEY_NAME = {
basic: "Basic Auth",
bearer: "Bearer",
"oauth-2": "OAuth 2.0",
"api-key": "API key",
none: "None",
} as const
const authType = pluckRef(auth, "authType")
const authName = computed(() =>
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
) )
const authType = pluckRef(auth, "authType")
const authName = computed(() => {
if (authType.value === "basic") return "Basic Auth"
else if (authType.value === "bearer") return "Bearer"
else if (authType.value === "oauth-2") return "OAuth 2.0"
else if (authType.value === "api-key") return "API key"
else return "None"
})
const authActive = pluckRef(auth, "authActive") const authActive = pluckRef(auth, "authActive")
const basicUsername = pluckRef(auth as Ref<HoppGQLAuthBasic>, "username")
const basicPassword = pluckRef(auth as Ref<HoppGQLAuthBasic>, "password")
const bearerToken = pluckRef(auth as Ref<HoppGQLAuthBearer>, "token")
const oauth2Token = pluckRef(auth as Ref<HoppGQLAuthOAuth2>, "token")
const apiKey = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "key")
const apiValue = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "value")
const addTo = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "addTo")
if (typeof addTo.value === "undefined") {
addTo.value = "Headers"
apiKey.value = ""
apiValue.value = ""
}
const clearContent = () => { const clearContent = () => {
auth.value = { auth.value = {
@@ -239,4 +316,5 @@ const clearContent = () => {
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
const authTippyActions = ref<any | null>(null)
</script> </script>

View File

@@ -1,430 +0,0 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("tab.headers") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<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' }"
:title="t('state.bulk_mode')"
:icon="IconEdit"
:class="{ '!text-accent': bulkMode }"
@click="bulkMode = !bulkMode"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
:disabled="bulkMode"
@click="addHeader"
/>
</div>
</div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
<div v-else>
<draggable
v-model="workingHeaders"
:item-key="(header: any) => `header-${header.id}`"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<HoppButtonSecondary
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
content:
index !== workingHeaders?.length - 1
? t('action.drag_to_reorder')
: null,
}"
:icon="IconGripVertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingHeaders?.length - 1,
}"
tabindex="-1"
/>
</span>
<HoppSmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
"
class="flex-1 !flex"
@input="
updateHeader(index, {
id: header.id,
key: $event,
value: header.value,
active: header.active,
})
"
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateHeader(index, {
id: header.id,
key: header.key,
value: ($event!.target! as HTMLInputElement).value,
active: header.active,
})
"
/>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
header.hasOwnProperty('active')
? header.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:icon="
header.hasOwnProperty('active')
? header.active
? IconCheckCircle
: IconCircle
: IconCheckCircle
"
color="green"
@click="
updateHeader(index, {
id: header.id,
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteHeader(index)"
/>
</span>
</div>
</template>
</draggable>
<HoppSmartPlaceholder
v-if="workingHeaders.length === 0"
:src="`/images/states/${colorMode.value}/add_category.svg`"
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addHeader"
/>
</HoppSmartPlaceholder>
</div>
</template>
<script setup lang="ts">
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconPlus from "~icons/lucide/plus"
import IconGripVertical from "~icons/lucide/grip-vertical"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconTrash from "~icons/lucide/trash"
import IconCircle from "~icons/lucide/circle"
import IconWrapText from "~icons/lucide/wrap-text"
import { reactive, ref, watch } from "vue"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import { pipe, flow } from "fp-ts/function"
import {
GQLHeader,
rawKeyValueEntriesToString,
parseRawKeyValueEntriesE,
RawKeyValueEntry,
HoppGQLRequest,
} from "@hoppscotch/data"
import draggable from "vuedraggable-es"
import { clone, cloneDeep, isEqual } from "lodash-es"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { commonHeaders } from "~/helpers/headers"
import { useCodemirror } from "@composables/codemirror"
import { objRemoveKey } from "~/helpers/functional/object"
import { useVModel } from "@vueuse/core"
const colorMode = useColorMode()
const t = useI18n()
const toast = useToast()
// v-model integration with props and emit
const props = defineProps<{ modelValue: HoppGQLRequest }>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLRequest): void
}>()
const request = useVModel(props, "modelValue", emit)
const idTicker = ref(0)
const linewrapEnabled = ref(false)
const bulkMode = ref(false)
const bulkHeaders = ref("")
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const bulkEditor = ref<any | null>(null)
useCodemirror(
bulkEditor,
bulkHeaders,
reactive({
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
// The UI representation of the headers list (has the empty end header)
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Rule: Working Headers always have one empty header or the last element is always an empty header
watch(workingHeaders, (headersList) => {
if (
headersList.length > 0 &&
headersList[headersList.length - 1].key !== ""
) {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
// Sync logic between headers and working headers
watch(
props.modelValue.headers,
(newHeadersList) => {
// Sync should overwrite working headers
const filteredWorkingHeaders = pipe(
workingHeaders.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(bulkHeaders.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
workingHeaders.value = pipe(
newHeadersList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
}
},
{ immediate: true }
)
watch(workingHeaders, (newWorkingHeaders) => {
const fixedHeaders = pipe(
newWorkingHeaders,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(request.value.headers, fixedHeaders)) {
request.value.headers = cloneDeep(fixedHeaders)
}
})
// Bulk Editor Syncing with Working Headers
watch(bulkHeaders, (newBulkHeaders) => {
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(newBulkHeaders),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(request.value.headers, filteredBulkHeaders)) {
request.value.headers = filteredBulkHeaders
}
})
watch(workingHeaders, (newHeadersList) => {
// If we are in bulk mode, don't apply direct changes
if (bulkMode.value) return
try {
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addHeader = () => {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
workingHeaders.value = workingHeaders.value.map((h, i) =>
i === index ? header : h
)
}
const deleteHeader = (index: number) => {
const headersBeforeDeletion = clone(workingHeaders.value)
if (
!(
headersBeforeDeletion.length > 0 &&
index === headersBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_: any, toastObject: any) => {
workingHeaders.value = headersBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingHeaders.value.splice(index, 1)
}
const clearContent = () => {
// set headers list to the initial state
workingHeaders.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkHeaders.value = ""
}
</script>

View File

@@ -1,235 +0,0 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.query") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-if="subscriptionState === 'SUBSCRIBED'"
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
allowHTML: true,
}"
:title="`${t('request.stop')}`"
:label="`${t('request.stop')}`"
:icon="IconStop"
class="rounded-none !text-accent !hover:text-accentDark"
@click="unsubscribe()"
/>
<HoppButtonSecondary
v-if="selectedOperation && subscriptionState !== 'SUBSCRIBED'"
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
allowHTML: true,
}"
:title="`${t('request.run')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
:icon="IconPlay"
:disabled="!selectedOperation"
class="rounded-none !text-accent !hover:text-accentDark"
@click="runQuery(selectedOperation)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="`${t('request.save')}`"
:icon="IconSave"
class="rounded-none"
@click="saveRequest"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLQuery()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyQueryIcon"
@click="prettifyQuery"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyQueryIcon"
@click="copyQuery"
/>
</div>
</div>
<div ref="queryEditor" class="flex flex-col flex-1"></div>
</template>
<script setup lang="ts">
import IconPlay from "~icons/lucide/play"
import IconStop from "~icons/lucide/stop-circle"
import IconSave from "~icons/lucide/save"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconInfo from "~icons/lucide/info"
import IconWand from "~icons/lucide/wand"
import IconWrapText from "~icons/lucide/wrap-text"
import { onMounted, reactive, ref, markRaw } from "vue"
import { copyToClipboard } from "@helpers/utils/clipboard"
import { useCodemirror } from "@composables/codemirror"
import { useI18n } from "@composables/i18n"
import { refAutoReset, useVModel } from "@vueuse/core"
import { useToast } from "~/composables/toast"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import * as gql from "graphql"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
import { selectedGQLOpHighlight } from "~/helpers/editor/gql/operation"
import { debounce } from "lodash-es"
import { ViewUpdate } from "@codemirror/view"
import { defineActionHandler } from "~/helpers/actions"
import {
schema,
socketDisconnect,
subscriptionState,
} from "~/helpers/graphql/connection"
// Template refs
const queryEditor = ref<any | null>(null)
const t = useI18n()
const toast = useToast()
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: "save-request"): void
(e: "update:modelValue", val: string): void
(e: "run-query", definition: gql.OperationDefinitionNode | null): void
}>()
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const prettifyQueryIcon = refAutoReset<
typeof IconWand | typeof IconCheck | typeof IconInfo
>(IconWand, 1000)
const linewrapEnabled = ref(true)
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
const gqlQueryString = useVModel(props, "modelValue", emit)
const debouncedOnUpdateQueryState = debounce((update: ViewUpdate) => {
const selectedPos = update.state.selection.main.head
const queryString = update.state.doc.toJSON().join(update.state.lineBreak)
try {
const operations = gql.parse(queryString)
if (operations.definitions.length === 1) {
selectedOperation.value = operations
.definitions[0] as gql.OperationDefinitionNode
return
}
selectedOperation.value =
(operations.definitions.find((def) => {
if (def.kind !== "OperationDefinition") return false
const { start, end } = def.loc!
return selectedPos >= start && selectedPos <= end
}) as gql.OperationDefinitionNode) ?? null
} catch (error) {
// console.error(error)
}
}, 300)
onMounted(() => {
try {
const operations = gql.parse(gqlQueryString.value)
if (operations.definitions.length) {
selectedOperation.value = operations
.definitions[0] as gql.OperationDefinitionNode
return
}
} catch (error) {}
})
useCodemirror(
queryEditor,
gqlQueryString,
reactive({
extendedEditorConfig: {
mode: "graphql",
placeholder: `${t("request.query")}`,
lineWrapping: linewrapEnabled,
},
linter: createGQLQueryLinter(schema),
completer: queryCompleter(schema),
environmentHighlights: false,
additionalExts: [markRaw(selectedGQLOpHighlight)],
onUpdate: debouncedOnUpdateQueryState,
})
)
// operations on graphql query string
// const operations = useReadonlyStream(props.request.operations$, [])
const prettifyQuery = () => {
try {
gqlQueryString.value = gql.print(
gql.parse(gqlQueryString.value, {
allowLegacyFragmentVariables: true,
})
)
prettifyQueryIcon.value = IconCheck
} catch (e) {
toast.error(`${t("error.gql_prettify_invalid_query")}`)
prettifyQueryIcon.value = IconInfo
}
}
const copyQuery = () => {
copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const clearGQLQuery = () => {
gqlQueryString.value = ""
}
const runQuery = (definition: gql.OperationDefinitionNode | null = null) => {
emit("run-query", definition)
}
const unsubscribe = () => {
socketDisconnect()
}
const saveRequest = () => {
emit("save-request")
}
defineActionHandler("editor.format", prettifyQuery)
</script>

View File

@@ -17,136 +17,55 @@
<HoppButtonPrimary <HoppButtonPrimary
id="get" id="get"
name="get" name="get"
:loading="connection.state === 'CONNECTING'" :loading="isLoading"
:label="!connected ? t('action.connect') : t('action.disconnect')" :label="!connected ? t('action.connect') : t('action.disconnect')"
class="w-32" class="w-32"
@click="onConnectClick" @click="onConnectClick"
/> />
</div> </div>
</div> </div>
<HoppSmartModal
v-if="connectionSwitchModal"
dialog
:dimissible="false"
:title="t('graphql.switch_connection')"
@close="connectionSwitchModal = false"
>
<template #body>
<p class="mb-4">
{{ t("graphql.connection_switch_url") }}:
<kbd class="shortcut-key !ml-0"> {{ lastTwoUrls.at(0) }} </kbd>
</p>
<p class="mb-4">
{{ t("graphql.connection_switch_new_url") }}:
<kbd class="shortcut-key !ml-0"> {{ lastTwoUrls.at(1) }} </kbd>
</p>
<p>{{ t("graphql.connection_switch_confirm") }}</p>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
:label="t('action.connect')"
:loading="connection.state === 'CONNECTING'"
outline
@click="switchConnection()"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="cancelSwitch()"
/>
</span>
</template>
</HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { platform } from "~/platform" import { platform } from "~/platform"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { currentActiveTab } from "~/helpers/graphql/tab" import {
import { computed, ref, watch } from "vue" gqlAuth$,
import { connection } from "~/helpers/graphql/connection" gqlHeaders$,
import { connect } from "~/helpers/graphql/connection" gqlURL$,
import { disconnect } from "~/helpers/graphql/connection" setGQLURL,
import { InterceptorService } from "~/services/interceptor.service" } from "~/newstore/GQLSession"
import { useService } from "dioc/vue"
import { defineActionHandler } from "~/helpers/actions"
const t = useI18n() const t = useI18n()
const interceptorService = useService(InterceptorService) const props = defineProps<{
conn: GQLConnection
}>()
const connectionSwitchModal = ref(false) const connected = useReadonlyStream(props.conn.connected$, false)
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
const connected = computed(() => connection.state === "CONNECTED") const headers = useReadonlyStream(gqlHeaders$, [])
const auth = useReadonlyStream(gqlAuth$, {
const url = computed({ authType: "none",
get: () => currentActiveTab.value?.document.request.url ?? "", authActive: true,
set: (value) => {
currentActiveTab.value!.document.request.url = value
},
}) })
const url = useStream(gqlURL$, "", setGQLURL)
const onConnectClick = () => { const onConnectClick = () => {
if (!connected.value) { if (!connected.value) {
gqlConnect() props.conn.connect(url.value, headers.value as any, auth.value)
platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
platform: "graphql-schema",
strategy: getCurrentStrategyID(),
})
} else { } else {
disconnect() props.conn.disconnect()
} }
} }
const gqlConnect = () => {
connect(url.value, currentActiveTab.value?.document.request.headers)
platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
platform: "graphql-schema",
strategy: interceptorService.currentInterceptorID.value!,
})
}
const switchConnection = () => {
gqlConnect()
connectionSwitchModal.value = false
}
const lastTwoUrls = ref<string[]>([])
watch(
currentActiveTab,
(newVal) => {
if (newVal) {
lastTwoUrls.value.push(newVal.document.request.url)
if (lastTwoUrls.value.length > 2) {
lastTwoUrls.value.shift()
}
}
if (
connected.value &&
lastTwoUrls.value.length === 2 &&
lastTwoUrls.value.at(0) !== lastTwoUrls.value.at(1)
) {
disconnect()
connectionSwitchModal.value = true
}
},
{
immediate: true,
}
)
const cancelSwitch = () => {
if (connected.value) disconnect()
connectionSwitchModal.value = false
}
defineActionHandler(
"gql.connect",
gqlConnect,
computed(() => !connected.value)
)
defineActionHandler("gql.disconnect", disconnect, connected)
</script> </script>

View File

@@ -2,42 +2,311 @@
<div class="flex flex-col flex-1 h-full"> <div class="flex flex-col flex-1 h-full">
<HoppSmartTabs <HoppSmartTabs
v-model="selectedOptionTab" v-model="selectedOptionTab"
styles="sticky top-0 bg-primary z-10 border-b-0" styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperPrimaryStickyFold z-10"
:render-inactive-tabs="true" render-inactive-tabs
> >
<HoppSmartTab <HoppSmartTab
:id="'query'" :id="'query'"
:label="`${t('tab.query')}`" :label="`${t('tab.query')}`"
:indicator="request.query && request.query.length > 0 ? true : false" :indicator="gqlQueryString && gqlQueryString.length > 0 ? true : false"
> >
<GraphqlQuery <div
v-model="request.query" class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold gqlRunQuery"
@run-query="runQuery" >
@save-request="saveRequest" <label class="font-semibold truncate text-secondaryLight">
/> {{ t("request.query") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'request.run'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${t('request.run')}`"
:icon="IconPlay"
class="rounded-none !text-accent !hover:text-accentDark"
@click="runQuery()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="`${t('request.save')}`"
:icon="IconSave"
class="rounded-none"
@click="saveRequest"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLQuery()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabledQuery }"
:icon="IconWrapText"
@click.prevent="linewrapEnabledQuery = !linewrapEnabledQuery"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyQueryIcon"
@click="prettifyQuery"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyQueryIcon"
@click="copyQuery"
/>
</div>
</div>
<div ref="queryEditor" class="flex flex-col flex-1"></div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
:id="'variables'" :id="'variables'"
:label="`${t('tab.variables')}`" :label="`${t('tab.variables')}`"
:indicator=" :indicator="variableString && variableString.length > 0 ? true : false"
request.variables && request.variables.length > 0 ? true : false
"
> >
<GraphqlVariable <div
v-model="request.variables" class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
@run-query="runQuery" >
@save-request="saveRequest" <label class="font-semibold truncate text-secondaryLight">
/> {{ t("request.variables") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLVariables()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabledVariable }"
:icon="IconWrapText"
@click.prevent="
linewrapEnabledVariable = !linewrapEnabledVariable
"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyVariablesIcon"
@click="prettifyVariableString"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyVariablesIcon"
@click="copyVariables"
/>
</div>
</div>
<div ref="variableEditor" class="flex flex-col flex-1"></div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
:id="'headers'" :id="'headers'"
:label="`${t('tab.headers')}`" :label="`${t('tab.headers')}`"
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`" :info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
> >
<GraphqlHeaders v-model="request" /> <div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("tab.headers") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<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' }"
:title="t('state.bulk_mode')"
:icon="IconEdit"
:class="{ '!text-accent': bulkMode }"
@click="bulkMode = !bulkMode"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
:disabled="bulkMode"
@click="addHeader"
/>
</div>
</div>
<div
v-if="bulkMode"
ref="bulkEditor"
class="flex flex-col flex-1"
></div>
<div v-else>
<draggable
v-model="workingHeaders"
:item-key="(header) => `header-${header.id}`"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<HoppButtonSecondary
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
content:
index !== workingHeaders?.length - 1
? t('action.drag_to_reorder')
: null,
}"
:icon="IconGripVertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingHeaders?.length - 1,
}"
tabindex="-1"
/>
</span>
<HoppSmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
"
class="flex-1 !flex"
@input="
updateHeader(index, {
id: header.id,
key: $event,
value: header.value,
active: header.active,
})
"
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateHeader(index, {
id: header.id,
key: header.key,
value: ($event!.target! as HTMLInputElement).value,
active: header.active,
})
"
/>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
header.hasOwnProperty('active')
? header.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:icon="
header.hasOwnProperty('active')
? header.active
? IconCheckCircle
: IconCircle
: IconCheckCircle
"
color="green"
@click="
updateHeader(index, {
id: header.id,
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteHeader(index)"
/>
</span>
</div>
</template>
</draggable>
<HoppSmartPlaceholder
v-if="workingHeaders.length === 0"
:src="`/images/states/${colorMode.value}/add_category.svg`"
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
class="mb-4"
@click="addHeader"
/>
</HoppSmartPlaceholder>
</div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`"> <HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<GraphqlAuthorization v-model="request.auth" /> <GraphqlAuthorization />
</HoppSmartTab> </HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
<CollectionsSaveRequest <CollectionsSaveRequest
@@ -49,165 +318,481 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconPlay from "~icons/lucide/play"
import IconSave from "~icons/lucide/save"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconPlus from "~icons/lucide/plus"
import IconGripVertical from "~icons/lucide/grip-vertical"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconTrash from "~icons/lucide/trash"
import IconCircle from "~icons/lucide/circle"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconInfo from "~icons/lucide/info"
import IconWand2 from "~icons/lucide/wand-2"
import IconWrapText from "~icons/lucide/wrap-text"
import { Ref, computed, reactive, ref, watch } from "vue"
import * as gql from "graphql"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import { pipe, flow } from "fp-ts/function"
import {
GQLHeader,
makeGQLRequest,
rawKeyValueEntriesToString,
parseRawKeyValueEntriesE,
RawKeyValueEntry,
} from "@hoppscotch/data"
import draggable from "vuedraggable-es"
import { clone, cloneDeep, isEqual } from "lodash-es"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { completePageProgress, startPageProgress } from "@modules/loadingbar" import { startPageProgress, completePageProgress } from "@modules/loadingbar"
import * as gql from "graphql"
import { clone } from "lodash-es"
import { computed, ref, watch } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { HoppGQLRequest } from "@hoppscotch/data"
import { platform } from "~/platform"
import { currentActiveTab } from "~/helpers/graphql/tab"
import { computedWithControl } from "@vueuse/core"
import { import {
GQLResponseEvent, gqlAuth$,
runGQLOperation, gqlHeaders$,
gqlMessageEvent, gqlQuery$,
} from "~/helpers/graphql/connection" gqlResponse$,
import { useService } from "dioc/vue" gqlURL$,
import { InterceptorService } from "~/services/interceptor.service" gqlVariables$,
import { editGraphqlRequest } from "~/newstore/collections" setGQLAuth,
setGQLHeaders,
setGQLQuery,
setGQLResponse,
setGQLVariables,
} from "~/newstore/GQLSession"
import { commonHeaders } from "~/helpers/headers"
import { GQLConnection } from "~/helpers/GQLConnection"
import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
import { platform } from "~/platform"
import { getCurrentStrategyID } from "~/helpers/network"
import { useCodemirror } from "@composables/codemirror"
import jsonLinter from "~/helpers/editor/linting/json"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { objRemoveKey } from "~/helpers/functional/object"
export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization" type OptionTabs = "query" | "headers" | "variables" | "authorization"
const selectedOptionTab = ref<GQLOptionTabs>("query")
const interceptorService = useService(InterceptorService) const colorMode = useColorMode()
const selectedOptionTab = ref<OptionTabs>("query")
const t = useI18n() const t = useI18n()
const props = defineProps<{
conn: GQLConnection
}>()
const toast = useToast() const toast = useToast()
// v-model integration with props and emit const url = useReadonlyStream(gqlURL$, "")
const props = withDefaults( const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
defineProps<{ const variableString = useStream(gqlVariables$, "", setGQLVariables)
modelValue: HoppGQLRequest
response?: GQLResponseEvent[] | null const idTicker = ref(0)
tabId: string
}>(), const bulkMode = ref(false)
const bulkHeaders = ref("")
const bulkEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(
bulkEditor,
bulkHeaders,
reactive({
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
// The functional headers list (the headers actually in the system)
const headers = useStream(gqlHeaders$, [], setGQLHeaders) as Ref<GQLHeader[]>
const auth = useStream(
gqlAuth$,
{ authType: "none", authActive: true },
setGQLAuth
)
// The UI representation of the headers list (has the empty end header)
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
{ {
response: null, id: idTicker.value++,
} key: "",
) value: "",
const emit = defineEmits(["update:modelValue", "update:response"]) active: true,
const request = ref(props.modelValue)
watch(
() => request.value,
(newVal) => {
emit("update:modelValue", newVal)
}, },
{ deep: true } ])
// Rule: Working Headers always have one empty header or the last element is always an empty header
watch(workingHeaders, (headersList) => {
if (
headersList.length > 0 &&
headersList[headersList.length - 1].key !== ""
) {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
// Sync logic between headers and working headers
watch(
headers,
(newHeadersList) => {
// Sync should overwrite working headers
const filteredWorkingHeaders = pipe(
workingHeaders.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(bulkHeaders.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
workingHeaders.value = pipe(
newHeadersList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
}
},
{ immediate: true }
) )
const url = computedWithControl( watch(workingHeaders, (newWorkingHeaders) => {
() => currentActiveTab.value, const fixedHeaders = pipe(
() => currentActiveTab.value.document.request.url newWorkingHeaders,
) A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(headers.value, fixedHeaders)) {
headers.value = cloneDeep(fixedHeaders)
}
})
// Bulk Editor Syncing with Working Headers
watch(bulkHeaders, (newBulkHeaders) => {
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(newBulkHeaders),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(headers.value, filteredBulkHeaders)) {
headers.value = filteredBulkHeaders
}
})
watch(workingHeaders, (newHeadersList) => {
// If we are in bulk mode, don't apply direct changes
if (bulkMode.value) return
try {
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addHeader = () => {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
workingHeaders.value = workingHeaders.value.map((h, i) =>
i === index ? header : h
)
}
const deleteHeader = (index: number) => {
const headersBeforeDeletion = clone(workingHeaders.value)
if (
!(
headersBeforeDeletion.length > 0 &&
index === headersBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingHeaders.value = headersBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingHeaders.value.splice(index, 1)
}
const clearContent = () => {
// set headers list to the initial state
workingHeaders.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkHeaders.value = ""
}
const activeGQLHeadersCount = computed( const activeGQLHeadersCount = computed(
() => () =>
request.value.headers.filter( headers.value.filter((x) => x.active && (x.key !== "" || x.value !== ""))
(x) => x.active && (x.key !== "" || x.value !== "") .length
).length
) )
const variableEditor = ref<any | null>(null)
const linewrapEnabledVariable = ref(true)
useCodemirror(
variableEditor,
variableString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
placeholder: `${t("request.variables")}`,
lineWrapping: linewrapEnabledVariable,
},
linter: computed(() =>
variableString.value.length > 0 ? jsonLinter : null
),
completer: null,
environmentHighlights: false,
})
)
const queryEditor = ref<any | null>(null)
const schema = useReadonlyStream(props.conn.schema$, null, "noclone")
const linewrapEnabledQuery = ref(true)
useCodemirror(
queryEditor,
gqlQueryString,
reactive({
extendedEditorConfig: {
mode: "graphql",
placeholder: `${t("request.query")}`,
lineWrapping: linewrapEnabledQuery,
},
linter: createGQLQueryLinter(schema),
completer: queryCompleter(schema),
environmentHighlights: false,
})
)
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const prettifyQueryIcon = refAutoReset<
typeof IconWand2 | typeof IconCheck | typeof IconInfo
>(IconWand2, 1000)
const prettifyVariablesIcon = refAutoReset<
typeof IconWand2 | typeof IconCheck | typeof IconInfo
>(IconWand2, 1000)
const showSaveRequestModal = ref(false) const showSaveRequestModal = ref(false)
const runQuery = async (
definition: gql.OperationDefinitionNode | null = null const copyQuery = () => {
) => { copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const response = useStream(gqlResponse$, "", setGQLResponse)
const runQuery = async () => {
const startTime = Date.now() const startTime = Date.now()
startPageProgress() startPageProgress()
response.value = "loading"
try { try {
const runURL = clone(url.value) const runURL = clone(url.value)
const runHeaders = clone(request.value.headers) const runHeaders = clone(headers.value)
const runQuery = clone(request.value.query) const runQuery = clone(gqlQueryString.value)
const runVariables = clone(request.value.variables) const runVariables = clone(variableString.value)
const runAuth = clone(request.value.auth) const runAuth = clone(auth.value)
await runGQLOperation({ const responseText = await props.conn.runQuery(
name: request.value.name, runURL,
url: runURL, runHeaders,
headers: runHeaders, runQuery,
query: runQuery, runVariables,
variables: runVariables, runAuth
auth: runAuth, )
operationName: definition?.name?.value,
operationType: definition?.operation ?? "query",
})
const duration = Date.now() - startTime const duration = Date.now() - startTime
completePageProgress() completePageProgress()
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
addGraphqlHistoryEntry(
makeGQLHistoryEntry({
request: makeGQLRequest({
name: "",
url: runURL,
query: runQuery,
headers: runHeaders,
variables: runVariables,
auth: runAuth,
}),
response: response.value,
star: false,
})
)
toast.success(`${t("state.finished_in", { duration })}`) toast.success(`${t("state.finished_in", { duration })}`)
} catch (e: any) { } catch (e: any) {
console.log(e) response.value = `${e}`
// response.value = [`${e}`]
completePageProgress() completePageProgress()
toast.error( toast.error(
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`, `${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
{} {}
) )
console.error(e) console.error(e)
} }
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN", type: "HOPP_REQUEST_RUN",
platform: "graphql-query", platform: "graphql-query",
strategy: interceptorService.currentInterceptorID.value!, strategy: getCurrentStrategyID(),
}) })
} }
watch(
() => gqlMessageEvent.value,
(event) => {
if (event === "reset") {
emit("update:response", [])
return
}
try {
if (event?.operationType !== "subscription") {
// response.value = [event]
emit("update:response", [event])
} else {
emit("update:response", [...(props.response ?? []), event])
// TODO: subscription indicator??
}
} catch (error) {
console.log(error)
}
},
{ deep: true }
)
const hideRequestModal = () => { const hideRequestModal = () => {
showSaveRequestModal.value = false showSaveRequestModal.value = false
} }
const saveRequest = () => {
if (
currentActiveTab.value.document.saveContext &&
currentActiveTab.value.document.saveContext.originLocation ===
"user-collection"
) {
editGraphqlRequest(
currentActiveTab.value.document.saveContext.folderPath,
currentActiveTab.value.document.saveContext.requestIndex,
currentActiveTab.value.document.request
)
currentActiveTab.value.document.isDirty = false const prettifyQuery = () => {
} else { try {
showSaveRequestModal.value = true gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
prettifyQueryIcon.value = IconCheck
} catch (e) {
toast.error(`${t("error.gql_prettify_invalid_query")}`)
prettifyQueryIcon.value = IconInfo
} }
} }
const clearGQLQuery = () => {
request.value.query = "" const saveRequest = () => {
showSaveRequestModal.value = true
} }
const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const prettifyVariableString = () => {
try {
const jsonObj = JSON.parse(variableString.value)
variableString.value = JSON.stringify(jsonObj, null, 2)
prettifyVariablesIcon.value = IconCheck
} catch (e) {
console.error(e)
prettifyVariablesIcon.value = IconInfo
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
}
const clearGQLQuery = () => {
gqlQueryString.value = ""
}
const clearGQLVariables = () => {
variableString.value = ""
}
defineActionHandler("request.send-cancel", runQuery) defineActionHandler("request.send-cancel", runQuery)
defineActionHandler("request.save", saveRequest) defineActionHandler("request.save", saveRequest)
defineActionHandler("request.save-as", () => {
showSaveRequestModal.value = true
})
defineActionHandler("request.reset", clearGQLQuery) defineActionHandler("request.reset", clearGQLQuery)
defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionTab.value = tab as GQLOptionTabs
})
</script> </script>

View File

@@ -1,50 +0,0 @@
<template>
<AppPaneLayout layout-id="gql-primary">
<template #primary>
<GraphqlRequestOptions
v-model="tab.document.request"
v-model:response="tab.response"
:tab-id="tab.id"
/>
</template>
<template #secondary>
<GraphqlResponse :response="tab.response" />
</template>
</AppPaneLayout>
</template>
<script setup lang="ts">
import { useVModel } from "@vueuse/core"
import { cloneDeep } from "lodash-es"
import { watch } from "vue"
import { isEqualHoppGQLRequest } from "~/helpers/graphql"
import { HoppGQLTab } from "~/helpers/graphql/tab"
// TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppGQLTab }>()
const emit = defineEmits<{
(e: "update:modelValue", val: HoppGQLTab): void
}>()
const tab = useVModel(props, "modelValue", emit)
// TODO: Come up with a better dirty check
let oldRequest = cloneDeep(tab.value.document.request)
watch(
() => tab.value.document.request,
(updatedValue) => {
// TODO: Check equality of request
if (
!tab.value.document.isDirty &&
!isEqualHoppGQLRequest(oldRequest, updatedValue)
) {
tab.value.document.isDirty = true
}
oldRequest = cloneDeep(updatedValue)
},
{ deep: true }
)
</script>

View File

@@ -1,6 +1,14 @@
<template> <template>
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap"> <div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
<div v-if="response?.length === 1" class="flex flex-col flex-1"> <HoppSmartPlaceholder
v-if="responseString === 'loading'"
:text="t('state.loading')"
>
<template #icon>
<HoppSmartSpinner class="my-4" />
</template>
</HoppSmartPlaceholder>
<div v-else-if="responseString" class="flex flex-col flex-1">
<div <div
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight" class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
> >
@@ -29,18 +37,12 @@
'action.copy' 'action.copy'
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`" )} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
:icon="copyResponseIcon" :icon="copyResponseIcon"
@click="copyResponse(response[0].data)" @click="copyResponse"
/> />
</div> </div>
</div> </div>
<div ref="schemaEditor" class="flex flex-col flex-1"></div> <div ref="schemaEditor" class="flex flex-col flex-1"></div>
</div> </div>
<div
v-else-if="response && response?.length > 1"
class="flex flex-col flex-1"
>
<GraphqlSubscriptionLog :log="response" />
</div>
<AppShortcutsPrompt v-else class="p-4" /> <AppShortcutsPrompt v-else class="p-4" />
</div> </div>
</template> </template>
@@ -50,34 +52,22 @@ import IconWrapText from "~icons/lucide/wrap-text"
import IconDownload from "~icons/lucide/download" import IconDownload from "~icons/lucide/download"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import { computed, reactive, ref } from "vue" import { reactive, ref } from "vue"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { gqlResponse$ } from "~/newstore/GQLSession"
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"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const props = withDefaults( const responseString = useReadonlyStream(gqlResponse$, "")
defineProps<{
response: GQLResponseEvent[] | null
}>(),
{
response: null,
}
)
const responseString = computed(() => {
if (props.response?.length === 1) {
return JSON.stringify(JSON.parse(props.response[0].data), null, 2)
}
return ""
})
const schemaEditor = ref<any | null>(null) const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const linewrapEnabled = ref(true)
@@ -105,14 +95,14 @@ const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
1000 1000
) )
const copyResponse = (str: string) => { const copyResponse = () => {
copyToClipboard(str) copyToClipboard(responseString.value!)
copyResponseIcon.value = IconCheck copyResponseIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`) toast.success(`${t("state.copied_to_clipboard")}`)
} }
const downloadResponse = (str: string) => { const downloadResponse = () => {
const dataToWrite = str const dataToWrite = responseString.value
const file = new Blob([dataToWrite!], { type: "application/json" }) const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a") const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
@@ -128,14 +118,6 @@ const downloadResponse = (str: string) => {
}, 1000) }, 1000)
} }
defineActionHandler( defineActionHandler("response.file.download", () => downloadResponse())
"response.file.download", defineActionHandler("response.copy", () => copyResponse())
() => downloadResponse(responseString.value),
computed(() => !!props.response && props.response.length > 0)
)
defineActionHandler(
"response.copy",
() => copyResponse(responseString.value),
computed(() => !!props.response && props.response.length > 0)
)
</script> </script>

View File

@@ -5,6 +5,20 @@
vertical vertical
render-inactive-tabs render-inactive-tabs
> >
<HoppSmartTab
:id="'history'"
:icon="IconClock"
:label="`${t('tab.history')}`"
>
<History :page="'graphql'" @use-history="handleUseHistory" />
</HoppSmartTab>
<HoppSmartTab
:id="'collections'"
:icon="IconFolder"
:label="`${t('tab.collections')}`"
>
<CollectionsGraphql />
</HoppSmartTab>
<HoppSmartTab <HoppSmartTab
:id="'docs'" :id="'docs'"
:icon="IconBookOpen" :icon="IconBookOpen"
@@ -159,21 +173,6 @@
> >
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab
:id="'collections'"
:icon="IconFolder"
:label="`${t('tab.collections')}`"
>
<CollectionsGraphql />
</HoppSmartTab>
<HoppSmartTab
:id="'history'"
:icon="IconClock"
:label="`${t('tab.history')}`"
>
<History :page="'graphql'" />
</HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
</template> </template>
@@ -189,24 +188,29 @@ import IconCopy from "~icons/lucide/copy"
import IconBox from "~icons/lucide/box" import IconBox from "~icons/lucide/box"
import { computed, nextTick, reactive, ref } from "vue" import { computed, nextTick, reactive, ref } from "vue"
import { GraphQLField, GraphQLType } from "graphql" import { GraphQLField, GraphQLType } from "graphql"
import { map } from "rxjs/operators"
import { GQLHeader } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { GQLConnection } from "@helpers/GQLConnection"
import { copyToClipboard } from "@helpers/utils/clipboard" import { copyToClipboard } from "@helpers/utils/clipboard"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { import {
graphqlTypes, setGQLAuth,
mutationFields, setGQLHeaders,
queryFields, setGQLQuery,
schemaString, setGQLResponse,
subscriptionFields, setGQLURL,
} from "~/helpers/graphql/connection" setGQLVariables,
} from "~/newstore/GQLSession"
type NavigationTabs = "history" | "collection" | "docs" | "schema" type NavigationTabs = "history" | "collection" | "docs" | "schema"
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types" type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
const selectedNavigationTab = ref<NavigationTabs>("docs") const selectedNavigationTab = ref<NavigationTabs>("history")
const selectedGqlTab = ref<GqlTabs>("queries") const selectedGqlTab = ref<GqlTabs>("queries")
const t = useI18n() const t = useI18n()
@@ -266,8 +270,40 @@ function resolveRootType(type: GraphQLType) {
return t return t
} }
type GQLHistoryEntry = {
url: string
headers: GQLHeader[]
query: string
response: string
variables: string
}
const props = defineProps<{
conn: GQLConnection
}>()
const toast = useToast() const toast = useToast()
const queryFields = useReadonlyStream(
props.conn.queryFields$.pipe(map((x) => x ?? [])),
[]
)
const mutationFields = useReadonlyStream(
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
[]
)
const subscriptionFields = useReadonlyStream(
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
[]
)
const graphqlTypes = useReadonlyStream(
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
[]
)
const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>( const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>(
IconDownload, IconDownload,
1000 1000
@@ -354,6 +390,11 @@ const handleJumpToType = async (type: GraphQLType) => {
} }
} }
const schemaString = useReadonlyStream(
props.conn.schemaString$.pipe(map((x) => x ?? "")),
""
)
const schemaEditor = ref<any | null>(null) const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const linewrapEnabled = ref(true)
@@ -395,4 +436,23 @@ const copySchema = () => {
copyToClipboard(schemaString.value) copyToClipboard(schemaString.value)
copySchemaIcon.value = IconCheck copySchemaIcon.value = IconCheck
} }
const handleUseHistory = (entry: GQLHistoryEntry) => {
const url = entry.url
const headers = entry.headers
const gqlQueryString = entry.query
const variableString = entry.variables
const responseText = entry.response
setGQLURL(url)
setGQLHeaders(headers)
setGQLQuery(gqlQueryString)
setGQLVariables(variableString)
setGQLResponse(responseText)
setGQLAuth({
authType: "none",
authActive: true,
})
props.conn.reset()
}
</script> </script>

View File

@@ -1,125 +0,0 @@
<template>
<div ref="container" class="flex flex-col flex-1">
<div
class="sticky top-0 z-10 flex items-center justify-between flex-none pl-4 border-b bg-primary border-dividerLight"
>
<label for="log" class="py-2 font-semibold text-secondaryLight">
{{ "Subscription Log" }}
</label>
<div>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
:icon="IconTrash"
@click="emit('delete')"
/>
<HoppButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.scroll_to_top')"
:icon="IconArrowUp"
@click="scrollTo('top')"
/>
<HoppButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.scroll_to_bottom')"
:icon="IconArrowDown"
@click="scrollTo('bottom')"
/>
<HoppButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.autoscroll')"
:icon="IconChevronsDown"
:class="toggleAutoscrollColor"
@click="toggleAutoscroll()"
/>
</div>
</div>
<div
v-if="log.length !== 0"
ref="logs"
class="overflow-y-auto border-b border-dividerLight"
>
<div
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
>
<RealtimeLogEntry
v-for="(entry, index) in log"
:key="`entry-${index}`"
:is-open="log.length - 1 === index"
:entry="{ ts: entry.time, source: 'info', payload: entry.data }"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, PropType, computed, watch, Ref } from "vue"
import IconTrash from "~icons/lucide/trash"
import IconArrowUp from "~icons/lucide/arrow-up"
import IconArrowDown from "~icons/lucide/arrow-down"
import IconChevronsDown from "~icons/lucide/chevron-down"
import { useThrottleFn, useScroll } from "@vueuse/core"
import { useI18n } from "@composables/i18n"
import { GQLResponseEvent } from "~/helpers/graphql/connection"
const props = defineProps({
log: { type: Array as PropType<GQLResponseEvent[]>, default: () => [] },
title: {
type: String,
default: "",
},
})
const emit = defineEmits<{
(e: "delete"): void
}>()
const t = useI18n()
const container = ref<HTMLElement | null>(null)
const logs = ref<HTMLElement | null>(null)
const autoScrollEnabled = ref(true)
const logListScroll = useScroll(logs as Ref<HTMLElement>)
// Disable autoscroll when scrolling to top
watch(logListScroll.isScrolling, (isScrolling) => {
if (isScrolling && logListScroll.directions.top)
autoScrollEnabled.value = false
})
const scrollTo = (position: "top" | "bottom") => {
if (position === "top") {
logs.value?.scroll({
behavior: "smooth",
top: 0,
})
} else if (position === "bottom") {
logs.value?.scroll({
behavior: "smooth",
top: logs.value?.scrollHeight,
})
}
}
watch(
() => props.log,
useThrottleFn(() => {
if (autoScrollEnabled.value) scrollTo("bottom")
}, 200),
{ flush: "post" }
)
const toggleAutoscroll = () => {
autoScrollEnabled.value = !autoScrollEnabled.value
}
const toggleAutoscrollColor = computed(() =>
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
)
</script>

View File

@@ -1,118 +0,0 @@
<template>
<div
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="tab.document.request.name"
class="truncate px-2 flex items-center"
@dblclick="emit('open-rename-modal')"
@contextmenu.prevent="options?.tippy?.show()"
@click.middle="emit('close-tab')"
>
<tippy
ref="options"
trigger="manual"
interactive
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span class="leading-8 px-2 truncate">
{{ tab.document.request.name }}
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.r="renameAction?.$el.click()"
@keyup.d="duplicateAction?.$el.click()"
@keyup.w="closeAction?.$el.click()"
@keyup.x="closeOthersAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="renameAction"
:icon="IconFileEdit"
:label="t('request.rename')"
:shortcut="['R']"
@click="
() => {
emit('open-rename-modal')
hide()
}
"
/>
<HoppSmartItem
ref="duplicateAction"
:icon="IconCopy"
:label="t('tab.duplicate')"
:shortcut="['D']"
@click="
() => {
emit('duplicate-tab')
hide()
}
"
/>
<HoppSmartItem
v-if="isRemovable"
ref="closeAction"
:icon="IconXCircle"
:label="t('tab.close')"
:shortcut="['W']"
@click="
() => {
emit('close-tab')
hide()
}
"
/>
<HoppSmartItem
v-if="isRemovable"
ref="closeOthersAction"
:icon="IconXSquare"
:label="t('tab.close_others')"
:shortcut="['X']"
@click="
() => {
emit('close-other-tabs')
hide()
}
"
/>
</div>
</template>
</tippy>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy"
import { HoppGQLTab } from "~/helpers/graphql/tab"
const t = useI18n()
defineProps<{
tab: HoppGQLTab
isRemovable: boolean
}>()
const emit = defineEmits<{
(event: "open-rename-modal"): void
(event: "close-tab"): void
(event: "close-other-tabs"): void
(event: "duplicate-tab"): void
}>()
const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null)
const renameAction = ref<HTMLButtonElement | null>(null)
const closeAction = ref<HTMLButtonElement | null>(null)
const closeOthersAction = ref<HTMLButtonElement | null>(null)
const duplicateAction = ref<HTMLButtonElement | null>(null)
</script>

View File

@@ -55,48 +55,51 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
// TODO: TypeScript + Setup Script this at some point :)
import { defineComponent } from "vue"
import { import {
GraphQLEnumType, GraphQLEnumType,
GraphQLInputObjectType, GraphQLInputObjectType,
GraphQLInterfaceType, GraphQLInterfaceType,
} from "graphql" } from "graphql"
import { computed } from "vue"
const props = defineProps({ export default defineComponent({
gqlType: { props: {
type: Object, // eslint-disable-next-line vue/require-default-prop, vue/require-prop-types
required: true, gqlType: {},
gqlTypes: { type: Array, default: () => [] },
jumpTypeCallback: { type: Function, default: () => ({}) },
isHighlighted: { type: Boolean, default: false },
highlightedFields: { type: Array, default: () => [] },
},
computed: {
isInput() {
return this.gqlType instanceof GraphQLInputObjectType
},
isInterface() {
return this.gqlType instanceof GraphQLInterfaceType
},
isEnum() {
return this.gqlType instanceof GraphQLEnumType
},
interfaces() {
return (this.gqlType.getInterfaces && this.gqlType.getInterfaces()) || []
},
children() {
return this.gqlTypes.filter(
(type) =>
type.getInterfaces && type.getInterfaces().includes(this.gqlType)
)
},
},
methods: {
isFieldHighlighted({ field }) {
return !!this.highlightedFields.find(({ name }) => name === field.name)
},
}, },
gqlTypes: { type: Array, default: () => [] },
jumpTypeCallback: { type: Function, default: () => ({}) },
isHighlighted: { type: Boolean, default: false },
highlightedFields: { type: Array, default: () => [] },
}) })
const isInput = computed(() => {
return props.gqlType instanceof GraphQLInputObjectType
})
const isInterface = computed(() => {
return props.gqlType instanceof GraphQLInterfaceType
})
const isEnum = computed(() => {
return props.gqlType instanceof GraphQLEnumType
})
const interfaces = computed(() => {
return (props.gqlType.getInterfaces && props.gqlType.getInterfaces()) || []
})
const children = computed(() => {
return props.gqlTypes.filter(
(type) => type.getInterfaces && type.getInterfaces().includes(props.gqlType)
)
})
const isFieldHighlighted = ({ field }) => {
return !!props.highlightedFields.find(({ name }) => name === field.name)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,172 +0,0 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.variables") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-if="subscriptionState === 'SUBSCRIBED'"
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
allowHTML: true,
}"
:title="`${t('request.stop')}`"
:label="`${t('request.stop')}`"
:icon="IconStop"
class="rounded-none !text-accent !hover:text-accentDark"
@click="unsubscribe()"
/>
<HoppButtonSecondary
v-if="selectedOperation && subscriptionState !== 'SUBSCRIBED'"
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
allowHTML: true,
}"
:title="`${t('request.run')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
:icon="IconPlay"
:disabled="!selectedOperation"
class="rounded-none !text-accent !hover:text-accentDark"
@click="runQuery(selectedOperation)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLVariables()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyVariablesIcon"
@click="prettifyVariableString"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyVariablesIcon"
@click="copyVariables"
/>
</div>
</div>
<div ref="variableEditor" class="flex flex-col flex-1"></div>
</template>
<script setup lang="ts">
import IconPlay from "~icons/lucide/play"
import IconStop from "~icons/lucide/stop-circle"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconInfo from "~icons/lucide/info"
import IconWand from "~icons/lucide/wand"
import IconWrapText from "~icons/lucide/wrap-text"
import { computed, reactive, ref } from "vue"
import jsonLinter from "~/helpers/editor/linting/json"
import { copyToClipboard } from "@helpers/utils/clipboard"
import { useCodemirror } from "@composables/codemirror"
import * as gql from "graphql"
import { useI18n } from "@composables/i18n"
import { refAutoReset, useVModel } from "@vueuse/core"
import { useToast } from "~/composables/toast"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import {
socketDisconnect,
subscriptionState,
} from "~/helpers/graphql/connection"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: "save-request"): void
(e: "update:modelValue", val: string): void
(e: "run-query", definition: gql.OperationDefinitionNode | null): void
}>()
// Watch operations on graphql query string
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
const variableString = useVModel(props, "modelValue", emit)
const variableEditor = ref<any | null>(null)
const linewrapEnabled = ref(false)
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const prettifyVariablesIcon = refAutoReset<
typeof IconWand | typeof IconCheck | typeof IconInfo
>(IconWand, 1000)
useCodemirror(
variableEditor,
variableString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
placeholder: `${t("request.variables")}`,
lineWrapping: linewrapEnabled,
},
linter: computed(() =>
variableString.value.length > 0 ? jsonLinter : null
),
completer: null,
environmentHighlights: false,
})
)
const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const prettifyVariableString = () => {
try {
const jsonObj = JSON.parse(variableString.value)
variableString.value = JSON.stringify(jsonObj, null, 2)
prettifyVariablesIcon.value = IconCheck
} catch (e) {
console.error(e)
prettifyVariablesIcon.value = IconInfo
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
}
const clearGQLVariables = () => {
variableString.value = ""
}
const runQuery = (definition: gql.OperationDefinitionNode | null = null) => {
emit("run-query", definition)
}
const unsubscribe = () => {
socketDisconnect()
}
</script>

View File

@@ -56,6 +56,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { makeGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import { setGQLSession } from "~/newstore/GQLSession"
import { GQLHistoryEntry } from "~/newstore/history" import { GQLHistoryEntry } from "~/newstore/history"
import { shortDateTime } from "~/helpers/utils/date" import { shortDateTime } from "~/helpers/utils/date"
@@ -66,8 +69,6 @@ import IconMinimize2 from "~icons/lucide/minimize-2"
import IconMaximize2 from "~icons/lucide/maximize-2" import IconMaximize2 from "~icons/lucide/maximize-2"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { makeGQLRequest } from "@hoppscotch/data"
import { createNewTab } from "~/helpers/graphql/tab"
const t = useI18n() const t = useI18n()
@@ -93,16 +94,19 @@ const query = computed(() =>
) )
const useEntry = () => { const useEntry = () => {
createNewTab({ setGQLSession({
request: makeGQLRequest({ request: cloneDeep(
name: props.entry.request.name, makeGQLRequest({
url: props.entry.request.url, name: props.entry.request.name,
headers: props.entry.request.headers, url: props.entry.request.url,
query: props.entry.request.query, headers: props.entry.request.headers,
variables: props.entry.request.variables, query: props.entry.request.query,
auth: props.entry.request.auth, variables: props.entry.request.variables,
}), auth: props.entry.request.auth,
isDirty: false, })
),
schema: "",
response: props.entry.response,
}) })
} }
</script> </script>

View File

@@ -105,7 +105,6 @@
@toggle-star="toggleStar(entry.entry)" @toggle-star="toggleStar(entry.entry)"
@delete-entry="deleteHistory(entry.entry)" @delete-entry="deleteHistory(entry.entry)"
@use-entry="useHistory(toRaw(entry.entry))" @use-entry="useHistory(toRaw(entry.entry))"
@add-to-collection="addToCollection(entry.entry)"
/> />
</details> </details>
</div> </div>
@@ -177,7 +176,7 @@ 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 { createNewTab } from "~/helpers/rest/tab"
import { defineActionHandler, invokeAction } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
@@ -325,14 +324,6 @@ const deleteHistory = (entry: HistoryEntry) => {
toast.success(`${t("state.deleted")}`) toast.success(`${t("state.deleted")}`)
} }
const addToCollection = (entry: HistoryEntry) => {
if (props.page === "rest") {
invokeAction("request.save-as", {
request: entry.request,
})
}
}
const toggleStar = (entry: HistoryEntry) => { const toggleStar = (entry: HistoryEntry) => {
// History entry type specified because function does not know the type // History entry type specified because function does not know the type
if (props.page === "rest") if (props.page === "rest")

View File

@@ -1,8 +1,5 @@
<template> <template>
<div <div class="flex items-stretch group">
class="flex items-stretch group"
@contextmenu.prevent="options!.tippy.show()"
>
<span <span
v-tippy="{ theme: 'tooltip', delay: [500, 20] }" v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer" class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
@@ -29,39 +26,6 @@
{{ entry.request.endpoint }} {{ entry.request.endpoint }}
</span> </span>
</span> </span>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.s="addToCollectionAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="addToCollectionAction"
:icon="IconSave"
:label="`${t('collection.save_to_collection')}`"
:shortcut="['S']"
@click="
() => {
emit('add-to-collection')
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconTrash" :icon="IconTrash"
@@ -84,16 +48,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue" import { computed } from "vue"
import findStatusGroup from "~/helpers/findStatusGroup" import findStatusGroup from "~/helpers/findStatusGroup"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { RESTHistoryEntry } from "~/newstore/history" import { RESTHistoryEntry } from "~/newstore/history"
import { shortDateTime } from "~/helpers/utils/date" import { shortDateTime } from "~/helpers/utils/date"
import IconSave from "~icons/lucide/save"
import IconStar from "~icons/lucide/star" import IconStar from "~icons/lucide/star"
import IconStarOff from "~icons/hopp/star-off" import IconStarOff from "~icons/hopp/star-off"
import IconTrash from "~icons/lucide/trash" import IconTrash from "~icons/lucide/trash"
import { TippyComponent } from "vue-tippy"
const props = defineProps<{ const props = defineProps<{
entry: RESTHistoryEntry entry: RESTHistoryEntry
@@ -104,13 +67,8 @@ const emit = defineEmits<{
(e: "use-entry"): void (e: "use-entry"): void
(e: "delete-entry"): void (e: "delete-entry"): void
(e: "toggle-star"): void (e: "toggle-star"): void
(e: "add-to-collection"): void
}>() }>()
const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null)
const addToCollectionAction = ref<HTMLButtonElement | null>(null)
const t = useI18n() const t = useI18n()
const duration = computed(() => { const duration = computed(() => {

View File

@@ -126,6 +126,7 @@
blank blank
:icon="IconExternalLink" :icon="IconExternalLink"
reverse reverse
class="mb-4"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight"> <div v-else class="flex flex-1 border-b border-dividerLight">

View File

@@ -28,9 +28,7 @@
> >
<HoppSmartItem <HoppSmartItem
:label="t('state.none')" :label="t('state.none')"
:info-icon=" :info-icon="(body.contentType === null ? IconDone : null) as any"
(body.contentType === null ? IconDone : null) as any
"
:active-info-icon="body.contentType === null" :active-info-icon="body.contentType === null"
@click=" @click="
() => { () => {
@@ -117,6 +115,7 @@
blank blank
:icon="IconExternalLink" :icon="IconExternalLink"
reverse reverse
class="mb-4"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</div> </div>

View File

@@ -162,6 +162,7 @@
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
:icon="IconPlus" :icon="IconPlus"
class="mb-4"
@click="addBodyParam" @click="addBodyParam"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>

View File

@@ -20,7 +20,7 @@
<span class="select-wrapper"> <span class="select-wrapper">
<HoppButtonSecondary <HoppButtonSecondary
:label=" :label="
CodegenDefinitions.find((x) => x.name === codegenType)!.caption CodegenDefinitions.find((x) => x.name === codegenType).caption
" "
outline outline
class="flex-1 pr-8" class="flex-1 pr-8"
@@ -57,7 +57,12 @@
" "
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="filteredCodegenDefinitions.length === 0" v-if="
!(
filteredCodegenDefinitions.length !== 0 ||
CodegenDefinitions.length === 0
)
"
:text="`${t('state.nothing_found')}${searchQuery}`" :text="`${t('state.nothing_found')}${searchQuery}`"
> >
<template #icon> <template #icon>

View File

@@ -79,13 +79,16 @@
tabindex="-1" tabindex="-1"
/> />
</span> </span>
<SmartEnvInput <HoppSmartAutoComplete
v-model="header.key"
:placeholder="`${t('count.header', { count: index + 1 })}`" :placeholder="`${t('count.header', { count: index + 1 })}`"
:auto-complete-source="commonHeaders" :source="commonHeaders"
:env-index="index" :spellcheck="false"
:inspection-results="getInspectorResult(headerKeyResults, index)" :value="header.key"
@change=" autofocus
styles=" bg-transparent flex flex-1
py-1 px-4 truncate "
class="flex-1 !flex"
@input="
updateHeader(index, { updateHeader(index, {
id: header.id, id: header.id,
key: $event, key: $event,
@@ -97,10 +100,6 @@
<SmartEnvInput <SmartEnvInput
v-model="header.value" v-model="header.value"
:placeholder="`${t('count.value', { count: index + 1 })}`" :placeholder="`${t('count.value', { count: index + 1 })}`"
:inspection-results="
getInspectorResult(headerValueResults, index)
"
:env-index="index"
@change=" @change="
updateHeader(index, { updateHeader(index, {
id: header.id, id: header.id,
@@ -213,6 +212,7 @@
filled filled
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
:icon="IconPlus" :icon="IconPlus"
class="mb-4"
@click="addHeader" @click="addHeader"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -265,9 +265,6 @@ import {
} from "~/helpers/utils/EffectiveURL" } from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments" import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -505,26 +502,4 @@ const changeTab = (tab: ComputedHeader["source"]) => {
if (tab === "auth") emit("change-tab", "authorization") if (tab === "auth") emit("change-tab", "authorization")
else emit("change-tab", "bodyParams") else emit("change-tab", "bodyParams")
} }
const inspectionService = useService(InspectionService)
const headerKeyResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "header" && result.locations.position === "key"
)
const headerValueResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "header" && result.locations.position === "value"
)
const getInspectorResult = (results: InspectorResult[], index: number) => {
return results.filter((result) => {
if (result.locations.type === "url" || result.locations.type === "response")
return
return result.locations.index === index
})
}
</script> </script>

View File

@@ -33,11 +33,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from "vue" import { ref, watch } from "vue"
import { import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
HoppGQLAuthOAuth2,
HoppRESTAuthOAuth2,
parseTemplateString,
} from "@hoppscotch/data"
import { pluckRef } from "@composables/ref" import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
@@ -48,7 +44,7 @@ const t = useI18n()
const toast = useToast() const toast = useToast()
const props = defineProps<{ const props = defineProps<{
modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2 modelValue: HoppRESTAuthOAuth2
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -82,9 +82,6 @@
<SmartEnvInput <SmartEnvInput
v-model="param.key" v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`" :placeholder="`${t('count.parameter', { count: index + 1 })}`"
:inspection-results="
getInspectorResult(parameterKeyResults, index)
"
@change=" @change="
updateParam(index, { updateParam(index, {
id: param.id, id: param.id,
@@ -97,9 +94,6 @@
<SmartEnvInput <SmartEnvInput
v-model="param.value" v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`" :placeholder="`${t('count.value', { count: index + 1 })}`"
:inspection-results="
getInspectorResult(parameterValueResults, index)
"
@change=" @change="
updateParam(index, { updateParam(index, {
id: param.id, id: param.id,
@@ -161,6 +155,7 @@
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
:icon="IconPlus" :icon="IconPlus"
filled filled
class="mb-4"
@click="addParam" @click="addParam"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -200,9 +195,6 @@ import { useToast } from "@composables/toast"
import { throwError } from "@functional/error" import { throwError } from "@functional/error"
import { objRemoveKey } from "@functional/object" import { objRemoveKey } from "@functional/object"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab"
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -406,27 +398,4 @@ const clearContent = () => {
bulkParams.value = "" bulkParams.value = ""
} }
const inspectionService = useService(InspectionService)
const parameterKeyResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "parameter" && result.locations.position === "key"
)
const parameterValueResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "parameter" &&
result.locations.position === "value"
)
const getInspectorResult = (results: InspectorResult[], index: number) => {
return results.filter((result) => {
if (result.locations.type === "url" || result.locations.type === "response")
return
return result.locations.index === index
})
}
</script> </script>

View File

@@ -53,7 +53,6 @@
v-model="tab.document.request.endpoint" v-model="tab.document.request.endpoint"
:placeholder="`${t('request.url')}`" :placeholder="`${t('request.url')}`"
:auto-complete-source="userHistories" :auto-complete-source="userHistories"
:inspection-results="tabResults"
@paste="onPasteUrl($event)" @paste="onPasteUrl($event)"
@enter="newSendRequest" @enter="newSendRequest"
/> />
@@ -63,9 +62,7 @@
<HoppButtonPrimary <HoppButtonPrimary
id="send" id="send"
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }" v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t( :title="`${t('action.send')} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
'action.send'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${!loading ? t('action.send') : t('action.cancel')}`" :label="`${!loading ? t('action.send') : t('action.cancel')}`"
class="flex-1 rounded-r-none min-w-20" class="flex-1 rounded-r-none min-w-20"
@click="!loading ? newSendRequest() : cancelRequest()" @click="!loading ? newSendRequest() : cancelRequest()"
@@ -224,7 +221,6 @@
v-if="showSaveRequestModal" v-if="showSaveRequestModal"
mode="rest" mode="rest"
:show="showSaveRequestModal" :show="showSaveRequestModal"
:request="request"
@hide-modal="showSaveRequestModal = false" @hide-modal="showSaveRequestModal = false"
/> />
</div> </div>
@@ -237,12 +233,17 @@ import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { refAutoReset, useVModel } from "@vueuse/core" import { refAutoReset, useVModel } from "@vueuse/core"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { Ref, computed, onBeforeUnmount, ref } from "vue" import { isLeft, isRight } from "fp-ts/lib/Either"
import { computed, onBeforeUnmount, ref } from "vue"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { runMutation } from "~/helpers/backend/GQLClient" import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql" import { UpdateRequestDocument } from "~/helpers/backend/graphql"
import { createShortcode } from "~/helpers/backend/mutations/Shortcode" import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import {
cancelRunningExtensionRequest,
hasExtensionInstalled,
} from "~/helpers/strategies/ExtensionStrategy"
import { runRESTRequest$ } from "~/helpers/RequestRunner" import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
@@ -257,17 +258,13 @@ 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 { HoppRESTTab } 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"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data" import { getCurrentStrategyID } from "~/helpers/network"
import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service"
const t = useI18n() const t = useI18n()
const interceptorService = useService(InterceptorService)
const methods = [ const methods = [
"GET", "GET",
@@ -320,8 +317,6 @@ const saveRequestAction = ref<any | null>(null)
const history = useReadonlyStream<RESTHistoryEntry[]>(restHistory$, []) const history = useReadonlyStream<RESTHistoryEntry[]>(restHistory$, [])
const requestCancelFunc: Ref<(() => void) | null> = ref(null)
const userHistories = computed(() => { const userHistories = computed(() => {
return history.value.map((history) => history.request.endpoint).slice(0, 10) return history.value.map((history) => history.request.endpoint).slice(0, 10)
}) })
@@ -340,15 +335,13 @@ const newSendRequest = async () => {
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN", type: "HOPP_REQUEST_RUN",
platform: "rest", platform: "rest",
strategy: interceptorService.currentInterceptorID.value!, strategy: getCurrentStrategyID(),
}) })
const [cancel, streamPromise] = runRESTRequest$(tab) // Double calling is because the function returns a TaskEither than should be executed
const streamResult = await streamPromise const streamResult = await runRESTRequest$(tab)()
requestCancelFunc.value = cancel if (isRight(streamResult)) {
if (E.isRight(streamResult)) {
subscribeToStream( subscribeToStream(
streamResult.right, streamResult.right,
(responseState) => { (responseState) => {
@@ -365,7 +358,7 @@ const newSendRequest = async () => {
loading.value = false loading.value = false
} }
) )
} else { } else if (isLeft(streamResult)) {
loading.value = false loading.value = false
toast.error(`${t("error.script_fail")}`) toast.error(`${t("error.script_fail")}`)
let error: Error let error: Error
@@ -415,8 +408,9 @@ function isCURL(curl: string) {
const cancelRequest = () => { const cancelRequest = () => {
loading.value = false loading.value = false
requestCancelFunc.value?.() if (hasExtensionInstalled()) {
cancelRunningExtensionRequest()
}
updateRESTResponse(null) updateRESTResponse(null)
} }
@@ -584,8 +578,6 @@ const saveRequest = () => {
} }
} }
const request = ref<HoppRESTRequest | null>(null)
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (loading.value) cancelRequest() if (loading.value) cancelRequest()
}) })
@@ -601,22 +593,7 @@ defineActionHandler("request.method.prev", cycleUpMethod)
defineActionHandler("request.save", saveRequest) defineActionHandler("request.save", saveRequest)
defineActionHandler( defineActionHandler(
"request.save-as", "request.save-as",
( () => (showSaveRequestModal.value = true)
req:
| {
requestType: "rest"
request: HoppRESTRequest
}
| {
requestType: "gql"
request: HoppGQLRequest
}
) => {
showSaveRequestModal.value = true
if (req && req.requestType === "rest") {
request.value = req.request
}
}
) )
defineActionHandler("request.method.get", () => updateMethod("GET")) defineActionHandler("request.method.get", () => updateMethod("GET"))
defineActionHandler("request.method.post", () => updateMethod("POST")) defineActionHandler("request.method.post", () => updateMethod("POST"))
@@ -624,13 +601,6 @@ defineActionHandler("request.method.put", () => updateMethod("PUT"))
defineActionHandler("request.method.delete", () => updateMethod("DELETE")) defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
defineActionHandler("request.method.head", () => updateMethod("HEAD")) defineActionHandler("request.method.head", () => updateMethod("HEAD"))
defineActionHandler("request.import-curl", () => {
showCurlImportModal.value = true
})
defineActionHandler("request.show-code", () => {
showCodegenModal.value = true
})
const isCustomMethod = computed(() => { const isCustomMethod = computed(() => {
return ( return (
tab.value.document.request.method === "CUSTOM" || tab.value.document.request.method === "CUSTOM" ||
@@ -639,8 +609,4 @@ const isCustomMethod = computed(() => {
}) })
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT") const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService)
const tabResults = inspectionService.getResultViewFor(currentTabID.value)
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<HoppSmartTabs <HoppSmartTabs
v-model="selectedOptionsTab" v-model="selectedRealtimeTab"
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
> >
@@ -56,15 +56,12 @@ 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, ref } from "vue" import { computed, ref } from "vue"
import { defineActionHandler } from "~/helpers/actions"
export type RequestOptionTabs = export type RequestOptionTabs =
| "params" | "params"
| "bodyParams" | "bodyParams"
| "headers" | "headers"
| "authorization" | "authorization"
| "preRequestScript"
| "tests"
const t = useI18n() const t = useI18n()
@@ -76,10 +73,10 @@ const emit = defineEmits<{
const request = useVModel(props, "modelValue", emit) const request = useVModel(props, "modelValue", emit)
const selectedOptionsTab = ref<RequestOptionTabs>("params") const selectedRealtimeTab = ref<RequestOptionTabs>("params")
const changeTab = (e: RequestOptionTabs) => { const changeTab = (e: RequestOptionTabs) => {
selectedOptionsTab.value = e selectedRealtimeTab.value = e
} }
const newActiveParamsCount$ = computed(() => { const newActiveParamsCount$ = computed(() => {
@@ -99,8 +96,4 @@ const newActiveHeadersCount$ = computed(() => {
if (e === 0) return null if (e === 0) return null
return `${e}` return `${e}`
}) })
defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionsTab.value = tab as RequestOptionTabs
})
</script> </script>

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