Compare commits

..

1 Commits

Author SHA1 Message Date
Nivedin
ca54c8498c feat: prettify XML response 2023-05-29 20:18:02 -04:00
116 changed files with 1602 additions and 2493 deletions

View File

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

View File

@@ -31,7 +31,6 @@ MICROSOFT_CLIENT_ID="************************************************"
MICROSOFT_CLIENT_SECRET="************************************************" MICROSOFT_CLIENT_SECRET="************************************************"
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback" MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
MICROSOFT_SCOPE="user.read" MICROSOFT_SCOPE="user.read"
MICROSOFT_TENANT="common"
# Mailer config # Mailer config
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com" MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
@@ -52,7 +51,7 @@ VITE_ADMIN_URL=http://localhost:3100
# Backend URLs # Backend URLs
VITE_BACKEND_GQL_URL=http://localhost:3170/graphql VITE_BACKEND_GQL_URL=http://localhost:3170/graphql
VITE_BACKEND_WS_URL=ws://localhost:3170/graphql VITE_BACKEND_WS_URL=wss://localhost:3170/graphql
VITE_BACKEND_API_URL=http://localhost:3170/v1 VITE_BACKEND_API_URL=http://localhost:3170/v1
# Terms Of Service And Privacy Policy Links (Optional) # Terms Of Service And Privacy Policy Links (Optional)

View File

@@ -1,42 +0,0 @@
name: Deploy to Netlify (ui)
on:
push:
branches: [main]
# run this workflow only if an update is made to the ui package
paths:
- "packages/hoppscotch-ui/**"
workflow_dispatch:
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
run: mv .env.example .env
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 8
run_install: true
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Build site
run: pnpm run generate-ui
# Deploy the ui site with netlify-cli
- name: Deploy to Netlify (ui)
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -19,12 +19,10 @@ services:
- 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=3000 - 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:3000" - "3170:3000"
@@ -62,20 +60,12 @@ services:
# 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

View File

@@ -11,7 +11,7 @@
"dev": "pnpm -r do-dev", "dev": "pnpm -r do-dev",
"gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl", "gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl",
"generate": "pnpm -r do-build-prod", "generate": "pnpm -r do-build-prod",
"start": "http-server packages/hoppscotch-selfhost-web/dist -p 3000", "start": "http-server packages/hoppscotch-web/dist -p 3000",
"lint": "pnpm -r do-lint", "lint": "pnpm -r do-lint",
"typecheck": "pnpm -r do-typecheck", "typecheck": "pnpm -r do-typecheck",
"lintfix": "pnpm -r do-lintfix", "lintfix": "pnpm -r do-lintfix",

View File

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

View File

@@ -181,7 +181,7 @@ export class AdminService {
* @returns an array team invitations * @returns an array team invitations
*/ */
async pendingInvitationCountInTeam(teamID: string) { async pendingInvitationCountInTeam(teamID: string) {
const invitations = await this.teamInvitationService.getTeamInvitations( const invitations = await this.teamInvitationService.getAllTeamInvitations(
teamID, teamID,
); );
@@ -236,11 +236,11 @@ export class AdminService {
const user = await this.userService.findUserByEmail(userEmail); const user = await this.userService.findUserByEmail(userEmail);
if (O.isNone(user)) return E.left(USER_NOT_FOUND); if (O.isNone(user)) return E.left(USER_NOT_FOUND);
const teamMember = await this.teamService.getTeamMemberTE( const isUserAlreadyMember = await this.teamService.getTeamMemberTE(
teamID, teamID,
user.value.uid, user.value.uid,
)(); )();
if (E.isLeft(teamMember)) { if (E.left(isUserAlreadyMember)) {
const addedUser = await this.teamService.addMemberToTeamWithEmail( const addedUser = await this.teamService.addMemberToTeamWithEmail(
teamID, teamID,
userEmail, userEmail,
@@ -248,18 +248,6 @@ export class AdminService {
); );
if (E.isLeft(addedUser)) return E.left(addedUser.left); if (E.isLeft(addedUser)) return E.left(addedUser.left);
const userInvitation =
await this.teamInvitationService.getTeamInviteByEmailAndTeamID(
userEmail,
teamID,
);
if (E.isRight(userInvitation)) {
await this.teamInvitationService.revokeInvitation(
userInvitation.right.id,
);
}
return E.right(addedUser.right); return E.right(addedUser.right);
} }

View File

@@ -228,7 +228,7 @@ export class AuthService {
url = process.env.VITE_BASE_URL; url = process.env.VITE_BASE_URL;
} }
await this.mailerService.sendEmail(email, { await this.mailerService.sendAuthEmail(email, {
template: 'code-your-own', template: 'code-your-own',
variables: { variables: {
inviteeEmail: email, inviteeEmail: email,

View File

@@ -17,7 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
clientSecret: process.env.MICROSOFT_CLIENT_SECRET, clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
callbackURL: process.env.MICROSOFT_CALLBACK_URL, callbackURL: process.env.MICROSOFT_CALLBACK_URL,
scope: [process.env.MICROSOFT_SCOPE], scope: [process.env.MICROSOFT_SCOPE],
tenant: process.env.MICROSOFT_TENANT, passReqToCallback: true,
store: true, store: true,
}); });
} }

View File

@@ -23,7 +23,7 @@ export const AUTH_FAIL = 'auth/fail';
export const JSON_INVALID = 'json_invalid'; export const JSON_INVALID = 'json_invalid';
/** /**
* Tried to delete a user data document from fb firestore but failed. * Tried to delete an user data document from fb firestore but failed.
* (FirebaseService) * (FirebaseService)
*/ */
export const USER_FB_DOCUMENT_DELETION_FAILED = export const USER_FB_DOCUMENT_DELETION_FAILED =
@@ -231,7 +231,7 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const; export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
/** /**
* Tried to perform an action on a request that doesn't accept their member role level * Tried to perform action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard) * (GqlRequestTeamMemberGuard)
*/ */
export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role'; export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role';
@@ -262,7 +262,7 @@ export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const; export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const;
/** /**
* Tried to perform an action on a request when the user is not even a member of the team * Tried to perform action on a request when the user is not even member of the team
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard) * (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
*/ */
export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member'; export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member';
@@ -307,18 +307,11 @@ export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const; export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
/** /**
* Invalid or non-existent TEAM ENVIRONMENT ID * Invalid or non-existent TEAM ENVIRONMMENT ID
* (TeamEnvironmentsService) * (TeamEnvironmentsService)
*/ */
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const; export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
/**
* Invalid TEAM ENVIRONMENT name
* (TeamEnvironmentsService)
*/
export const TEAM_ENVIRONMENT_SHORT_NAME =
'team_environment/short_name' as const;
/** /**
* The user is not a member of the team of the given environment * The user is not a member of the team of the given environment
* (GqlTeamEnvTeamGuard) * (GqlTeamEnvTeamGuard)
@@ -347,7 +340,7 @@ export const USER_SETTINGS_NULL_SETTINGS =
'user_settings/null_settings' as const; 'user_settings/null_settings' as const;
/* /*
* Global environment doesn't exist for the user * Global environment doesnt exists for the user
* (UserEnvironmentsService) * (UserEnvironmentsService)
*/ */
export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS = export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS =

View File

@@ -5,6 +5,7 @@ import {
UserMagicLinkMailDescription, UserMagicLinkMailDescription,
} from './MailDescriptions'; } from './MailDescriptions';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import * as TE from 'fp-ts/TaskEither';
import { EMAIL_FAILED } from 'src/errors'; import { EMAIL_FAILED } from 'src/errors';
import { MailerService as NestMailerService } from '@nestjs-modules/mailer'; import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
@@ -34,14 +35,33 @@ export class MailerService {
/** /**
* Sends an email to the given email address given a mail description * Sends an email to the given email address given a mail description
* @param to Receiver's email id * @param to The email address to be sent to (NOTE: this is not validated)
* @param mailDesc Definition of what email to be sent * @param mailDesc Definition of what email to be sent
* @returns Response if email was send successfully or not
*/ */
async sendEmail( sendMail(
to: string, to: string,
mailDesc: MailDescription | UserMagicLinkMailDescription, mailDesc: MailDescription | UserMagicLinkMailDescription,
) { ) {
return TE.tryCatch(
async () => {
await this.nestMailerService.sendMail({
to,
template: mailDesc.template,
subject: this.resolveSubjectForMailDesc(mailDesc),
context: mailDesc.variables,
});
},
() => EMAIL_FAILED,
);
}
/**
*
* @param to Receiver's email id
* @param mailDesc Details of email to be sent for Magic-Link auth
* @returns Response if email was send successfully or not
*/
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
try { try {
await this.nestMailerService.sendMail({ await this.nestMailerService.sendMail({
to, to,

View File

@@ -9,6 +9,7 @@ import { emitGQLSchemaFile } from './gql-schema';
async function bootstrap() { async function bootstrap() {
console.log(`Running in production: ${process.env.PRODUCTION}`); console.log(`Running in production: ${process.env.PRODUCTION}`);
console.log(`Port: ${process.env.PORT}`); console.log(`Port: ${process.env.PORT}`);
console.log(`Database: ${process.env.DATABASE_URL}`);
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);

View File

@@ -1,5 +1,15 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import * as TE from 'fp-ts/TaskEither';
import * as O from 'fp-ts/Option';
import * as S from 'fp-ts/string';
import { pipe } from 'fp-ts/function';
import {
getAnnotatedRequiredRoles,
getGqlArg,
getUserFromGQLContext,
throwErr,
} from 'src/utils';
import { TeamEnvironmentsService } from './team-environments.service'; import { TeamEnvironmentsService } from './team-environments.service';
import { import {
BUG_AUTH_NO_USER_CTX, BUG_AUTH_NO_USER_CTX,
@@ -9,10 +19,6 @@ import {
TEAM_ENVIRONMENT_NOT_FOUND, TEAM_ENVIRONMENT_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import { TeamService } from 'src/team/team.service'; import { TeamService } from 'src/team/team.service';
import { GqlExecutionContext } from '@nestjs/graphql';
import * as E from 'fp-ts/Either';
import { TeamMemberRole } from '@prisma/client';
import { throwErr } from 'src/utils';
/** /**
* A guard which checks whether the caller of a GQL Operation * A guard which checks whether the caller of a GQL Operation
@@ -27,31 +33,50 @@ export class GqlTeamEnvTeamGuard implements CanActivate {
private readonly teamService: TeamService, private readonly teamService: TeamService,
) {} ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { canActivate(context: ExecutionContext): Promise<boolean> {
const requireRoles = this.reflector.get<TeamMemberRole[]>( return pipe(
'requiresTeamRole', TE.Do,
context.getHandler(),
);
if (!requireRoles) throw new Error(BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES);
const gqlExecCtx = GqlExecutionContext.create(context); TE.bindW('requiredRoles', () =>
pipe(
getAnnotatedRequiredRoles(this.reflector, context),
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES),
),
),
const { user } = gqlExecCtx.getContext().req; TE.bindW('user', () =>
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX); pipe(
getUserFromGQLContext(context),
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
),
),
const { id } = gqlExecCtx.getArgs<{ id: string }>(); TE.bindW('envID', () =>
if (!id) throwErr(BUG_TEAM_ENV_GUARD_NO_ENV_ID); pipe(
getGqlArg('id', context),
O.fromPredicate(S.isString),
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID),
),
),
const teamEnvironment = TE.bindW('membership', ({ envID, user }) =>
await this.teamEnvironmentService.getTeamEnvironment(id); pipe(
if (E.isLeft(teamEnvironment)) throwErr(TEAM_ENVIRONMENT_NOT_FOUND); this.teamEnvironmentService.getTeamEnvironment(envID),
TE.fromTaskOption(() => TEAM_ENVIRONMENT_NOT_FOUND),
TE.chainW((env) =>
pipe(
this.teamService.getTeamMemberTE(env.teamID, user.uid),
TE.mapLeft(() => TEAM_ENVIRONMENT_NOT_TEAM_MEMBER),
),
),
),
),
const member = await this.teamService.getTeamMember( TE.map(({ membership, requiredRoles }) =>
teamEnvironment.right.teamID, requiredRoles.includes(membership.role),
user.uid, ),
);
if (!member) throwErr(TEAM_ENVIRONMENT_NOT_TEAM_MEMBER);
return requireRoles.includes(member.role); TE.getOrElse(throwErr),
)();
} }
} }

View File

@@ -1,41 +0,0 @@
import { ArgsType, Field, ID } from '@nestjs/graphql';
@ArgsType()
export class CreateTeamEnvironmentArgs {
@Field({
name: 'name',
description: 'Name of the Team Environment',
})
name: string;
@Field(() => ID, {
name: 'teamID',
description: 'ID of the Team',
})
teamID: string;
@Field({
name: 'variables',
description: 'JSON string of the variables object',
})
variables: string;
}
@ArgsType()
export class UpdateTeamEnvironmentArgs {
@Field(() => ID, {
name: 'id',
description: 'ID of the Team Environment',
})
id: string;
@Field({
name: 'name',
description: 'Name of the Team Environment',
})
name: string;
@Field({
name: 'variables',
description: 'JSON string of the variables object',
})
variables: string;
}

View File

@@ -13,11 +13,6 @@ import { throwErr } from 'src/utils';
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard'; import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
import { TeamEnvironment } from './team-environments.model'; import { TeamEnvironment } from './team-environments.model';
import { TeamEnvironmentsService } from './team-environments.service'; import { TeamEnvironmentsService } from './team-environments.service';
import * as E from 'fp-ts/Either';
import {
CreateTeamEnvironmentArgs,
UpdateTeamEnvironmentArgs,
} from './input-type.args';
@UseGuards(GqlThrottlerGuard) @UseGuards(GqlThrottlerGuard)
@Resolver(() => 'TeamEnvironment') @Resolver(() => 'TeamEnvironment')
@@ -34,18 +29,29 @@ export class TeamEnvironmentsResolver {
}) })
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async createTeamEnvironment( createTeamEnvironment(
@Args() args: CreateTeamEnvironmentArgs, @Args({
name: 'name',
description: 'Name of the Team Environment',
})
name: string,
@Args({
name: 'teamID',
description: 'ID of the Team',
type: () => ID,
})
teamID: string,
@Args({
name: 'variables',
description: 'JSON string of the variables object',
})
variables: string,
): Promise<TeamEnvironment> { ): Promise<TeamEnvironment> {
const teamEnvironment = return this.teamEnvironmentsService.createTeamEnvironment(
await this.teamEnvironmentsService.createTeamEnvironment( name,
args.name, teamID,
args.teamID, variables,
args.variables, )();
);
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
return teamEnvironment.right;
} }
@Mutation(() => Boolean, { @Mutation(() => Boolean, {
@@ -53,7 +59,7 @@ export class TeamEnvironmentsResolver {
}) })
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard) @UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async deleteTeamEnvironment( deleteTeamEnvironment(
@Args({ @Args({
name: 'id', name: 'id',
description: 'ID of the Team Environment', description: 'ID of the Team Environment',
@@ -61,12 +67,10 @@ export class TeamEnvironmentsResolver {
}) })
id: string, id: string,
): Promise<boolean> { ): Promise<boolean> {
const isDeleted = await this.teamEnvironmentsService.deleteTeamEnvironment( return pipe(
id, this.teamEnvironmentsService.deleteTeamEnvironment(id),
); TE.getOrElse(throwErr),
)();
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
return isDeleted.right;
} }
@Mutation(() => TeamEnvironment, { @Mutation(() => TeamEnvironment, {
@@ -75,19 +79,28 @@ export class TeamEnvironmentsResolver {
}) })
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard) @UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async updateTeamEnvironment( updateTeamEnvironment(
@Args() @Args({
args: UpdateTeamEnvironmentArgs, name: 'id',
description: 'ID of the Team Environment',
type: () => ID,
})
id: string,
@Args({
name: 'name',
description: 'Name of the Team Environment',
})
name: string,
@Args({
name: 'variables',
description: 'JSON string of the variables object',
})
variables: string,
): Promise<TeamEnvironment> { ): Promise<TeamEnvironment> {
const updatedTeamEnvironment = return pipe(
await this.teamEnvironmentsService.updateTeamEnvironment( this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables),
args.id, TE.getOrElse(throwErr),
args.name, )();
args.variables,
);
if (E.isLeft(updatedTeamEnvironment)) throwErr(updatedTeamEnvironment.left);
return updatedTeamEnvironment.right;
} }
@Mutation(() => TeamEnvironment, { @Mutation(() => TeamEnvironment, {
@@ -95,7 +108,7 @@ export class TeamEnvironmentsResolver {
}) })
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard) @UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async deleteAllVariablesFromTeamEnvironment( deleteAllVariablesFromTeamEnvironment(
@Args({ @Args({
name: 'id', name: 'id',
description: 'ID of the Team Environment', description: 'ID of the Team Environment',
@@ -103,13 +116,10 @@ export class TeamEnvironmentsResolver {
}) })
id: string, id: string,
): Promise<TeamEnvironment> { ): Promise<TeamEnvironment> {
const teamEnvironment = return pipe(
await this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment( this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id),
id, TE.getOrElse(throwErr),
); )();
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
return teamEnvironment.right;
} }
@Mutation(() => TeamEnvironment, { @Mutation(() => TeamEnvironment, {
@@ -117,7 +127,7 @@ export class TeamEnvironmentsResolver {
}) })
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard) @UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async createDuplicateEnvironment( createDuplicateEnvironment(
@Args({ @Args({
name: 'id', name: 'id',
description: 'ID of the Team Environment', description: 'ID of the Team Environment',
@@ -125,12 +135,10 @@ export class TeamEnvironmentsResolver {
}) })
id: string, id: string,
): Promise<TeamEnvironment> { ): Promise<TeamEnvironment> {
const res = await this.teamEnvironmentsService.createDuplicateEnvironment( return pipe(
id, this.teamEnvironmentsService.createDuplicateEnvironment(id),
); TE.getOrElse(throwErr),
)();
if (E.isLeft(res)) throwErr(res.left);
return res.right;
} }
/* Subscriptions */ /* Subscriptions */

View File

@@ -2,11 +2,7 @@ import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { TeamEnvironment } from './team-environments.model'; import { TeamEnvironment } from './team-environments.model';
import { TeamEnvironmentsService } from './team-environments.service'; import { TeamEnvironmentsService } from './team-environments.service';
import { import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
JSON_INVALID,
TEAM_ENVIRONMENT_NOT_FOUND,
TEAM_ENVIRONMENT_SHORT_NAME,
} from 'src/errors';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
@@ -35,81 +31,125 @@ beforeEach(() => {
describe('TeamEnvironmentsService', () => { describe('TeamEnvironmentsService', () => {
describe('getTeamEnvironment', () => { describe('getTeamEnvironment', () => {
test('should successfully return a TeamEnvironment with valid ID', async () => { test('queries the db with the id', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce( mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
teamEnvironment,
);
const result = await teamEnvironmentsService.getTeamEnvironment( await teamEnvironmentsService.getTeamEnvironment('123')();
teamEnvironment.id,
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: {
id: '123',
},
}),
); );
expect(result).toEqualRight(teamEnvironment);
}); });
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => { test('requests prisma to reject the query promise if not found', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce( mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
'RejectOnNotFound',
);
const result = await teamEnvironmentsService.getTeamEnvironment( await teamEnvironmentsService.getTeamEnvironment('123')();
teamEnvironment.id,
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
rejectOnNotFound: true,
}),
); );
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
});
}); });
test('should return a Some of the correct environment if exists', async () => {
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
expect(result).toEqualSome(teamEnvironment);
});
test('should return a None if the environment does not exist', async () => {
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
expect(result).toBeNone();
});
});
describe('createTeamEnvironment', () => { describe('createTeamEnvironment', () => {
test('should successfully create and return a new team environment given valid inputs', async () => { test('should create and return a new team environment given a valid name,variable and team ID', async () => {
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment); mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
const result = await teamEnvironmentsService.createTeamEnvironment( const result = await teamEnvironmentsService.createTeamEnvironment(
teamEnvironment.name, teamEnvironment.name,
teamEnvironment.teamID, teamEnvironment.teamID,
JSON.stringify(teamEnvironment.variables), JSON.stringify(teamEnvironment.variables),
); )();
expect(result).toEqualRight({ expect(result).toEqual(<TeamEnvironment>{
...teamEnvironment, id: teamEnvironment.id,
name: teamEnvironment.name,
teamID: teamEnvironment.teamID,
variables: JSON.stringify(teamEnvironment.variables), variables: JSON.stringify(teamEnvironment.variables),
}); });
}); });
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => { test('should reject if given team ID is invalid', async () => {
const result = await teamEnvironmentsService.createTeamEnvironment( mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
'12',
await expect(
teamEnvironmentsService.createTeamEnvironment(
teamEnvironment.name,
'invalidteamid',
JSON.stringify(teamEnvironment.variables),
),
).rejects.toBeDefined();
});
test('should reject if provided team environment name is not a string', async () => {
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
await expect(
teamEnvironmentsService.createTeamEnvironment(
null as any,
teamEnvironment.teamID, teamEnvironment.teamID,
JSON.stringify(teamEnvironment.variables), JSON.stringify(teamEnvironment.variables),
); ),
).rejects.toBeDefined();
});
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME); test('should reject if provided variable is not a string', async () => {
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
await expect(
teamEnvironmentsService.createTeamEnvironment(
teamEnvironment.name,
teamEnvironment.teamID,
null as any,
),
).rejects.toBeDefined();
}); });
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => { test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment); mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment);
const result = await teamEnvironmentsService.createTeamEnvironment( const result = await teamEnvironmentsService.createTeamEnvironment(
teamEnvironment.name, teamEnvironment.name,
teamEnvironment.teamID, teamEnvironment.teamID,
JSON.stringify(teamEnvironment.variables), JSON.stringify(teamEnvironment.variables),
); )();
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/created`, `team_environment/${teamEnvironment.teamID}/created`,
{ result,
...teamEnvironment,
variables: JSON.stringify(teamEnvironment.variables),
},
); );
}); });
}); });
describe('deleteTeamEnvironment', () => { describe('deleteTeamEnvironment', () => {
test('should successfully delete a TeamEnvironment with a valid ID', async () => { test('should resolve to true given a valid team environment ID', async () => {
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment); mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
const result = await teamEnvironmentsService.deleteTeamEnvironment( const result = await teamEnvironmentsService.deleteTeamEnvironment(
teamEnvironment.id, teamEnvironment.id,
); )();
expect(result).toEqualRight(true); expect(result).toEqualRight(true);
}); });
@@ -119,7 +159,7 @@ describe('TeamEnvironmentsService', () => {
const result = await teamEnvironmentsService.deleteTeamEnvironment( const result = await teamEnvironmentsService.deleteTeamEnvironment(
'invalidid', 'invalidid',
); )();
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
}); });
@@ -129,7 +169,7 @@ describe('TeamEnvironmentsService', () => {
const result = await teamEnvironmentsService.deleteTeamEnvironment( const result = await teamEnvironmentsService.deleteTeamEnvironment(
teamEnvironment.id, teamEnvironment.id,
); )();
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/deleted`, `team_environment/${teamEnvironment.teamID}/deleted`,
@@ -142,7 +182,7 @@ describe('TeamEnvironmentsService', () => {
}); });
describe('updateVariablesInTeamEnvironment', () => { describe('updateVariablesInTeamEnvironment', () => {
test('should successfully add new variable to a team environment', async () => { test('should add new variable to a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({ mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
...teamEnvironment, ...teamEnvironment,
variables: [{ key: 'value' }], variables: [{ key: 'value' }],
@@ -152,7 +192,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id, teamEnvironment.id,
teamEnvironment.name, teamEnvironment.name,
JSON.stringify([{ key: 'value' }]), JSON.stringify([{ key: 'value' }]),
); )();
expect(result).toEqualRight(<TeamEnvironment>{ expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment, ...teamEnvironment,
@@ -160,7 +200,7 @@ describe('TeamEnvironmentsService', () => {
}); });
}); });
test('should successfully add new variable to already existing list of variables in a team environment', async () => { test('should add new variable to already existing list of variables in a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({ mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
...teamEnvironment, ...teamEnvironment,
variables: [{ key: 'value' }, { key_2: 'value_2' }], variables: [{ key: 'value' }, { key_2: 'value_2' }],
@@ -170,7 +210,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id, teamEnvironment.id,
teamEnvironment.name, teamEnvironment.name,
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]), JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
); )();
expect(result).toEqualRight(<TeamEnvironment>{ expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment, ...teamEnvironment,
@@ -178,7 +218,7 @@ describe('TeamEnvironmentsService', () => {
}); });
}); });
test('should successfully edit existing variables in a team environment', async () => { test('should edit existing variables in a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({ mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
...teamEnvironment, ...teamEnvironment,
variables: [{ key: '1234' }], variables: [{ key: '1234' }],
@@ -188,7 +228,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id, teamEnvironment.id,
teamEnvironment.name, teamEnvironment.name,
JSON.stringify([{ key: '1234' }]), JSON.stringify([{ key: '1234' }]),
); )();
expect(result).toEqualRight(<TeamEnvironment>{ expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment, ...teamEnvironment,
@@ -196,7 +236,22 @@ describe('TeamEnvironmentsService', () => {
}); });
}); });
test('should successfully edit name of an existing team environment', async () => { test('should delete existing variable in a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
const result = await teamEnvironmentsService.updateTeamEnvironment(
teamEnvironment.id,
teamEnvironment.name,
JSON.stringify([{}]),
)();
expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment,
variables: JSON.stringify([{}]),
});
});
test('should edit name of an existing team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({ mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
...teamEnvironment, ...teamEnvironment,
variables: [{ key: '123' }], variables: [{ key: '123' }],
@@ -206,7 +261,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id, teamEnvironment.id,
teamEnvironment.name, teamEnvironment.name,
JSON.stringify([{ key: '123' }]), JSON.stringify([{ key: '123' }]),
); )();
expect(result).toEqualRight(<TeamEnvironment>{ expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment, ...teamEnvironment,
@@ -214,24 +269,14 @@ describe('TeamEnvironmentsService', () => {
}); });
}); });
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => { test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
const result = await teamEnvironmentsService.updateTeamEnvironment(
teamEnvironment.id,
'12',
JSON.stringify([{ key: 'value' }]),
);
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
});
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound'); mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
const result = await teamEnvironmentsService.updateTeamEnvironment( const result = await teamEnvironmentsService.updateTeamEnvironment(
'invalidid', 'invalidid',
teamEnvironment.name, teamEnvironment.name,
JSON.stringify(teamEnvironment.variables), JSON.stringify(teamEnvironment.variables),
); )();
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
}); });
@@ -243,7 +288,7 @@ describe('TeamEnvironmentsService', () => {
teamEnvironment.id, teamEnvironment.id,
teamEnvironment.name, teamEnvironment.name,
JSON.stringify([{ key: 'value' }]), JSON.stringify([{ key: 'value' }]),
); )();
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/updated`, `team_environment/${teamEnvironment.teamID}/updated`,
@@ -256,13 +301,13 @@ describe('TeamEnvironmentsService', () => {
}); });
describe('deleteAllVariablesFromTeamEnvironment', () => { describe('deleteAllVariablesFromTeamEnvironment', () => {
test('should successfully delete all variables in a team environment', async () => { test('should delete all variables in a team environment', async () => {
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment); mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
const result = const result =
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment( await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
teamEnvironment.id, teamEnvironment.id,
); )();
expect(result).toEqualRight(<TeamEnvironment>{ expect(result).toEqualRight(<TeamEnvironment>{
...teamEnvironment, ...teamEnvironment,
@@ -270,13 +315,13 @@ describe('TeamEnvironmentsService', () => {
}); });
}); });
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound'); mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
const result = const result =
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment( await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
'invalidid', 'invalidid',
); )();
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
}); });
@@ -287,7 +332,7 @@ describe('TeamEnvironmentsService', () => {
const result = const result =
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment( await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
teamEnvironment.id, teamEnvironment.id,
); )();
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/updated`, `team_environment/${teamEnvironment.teamID}/updated`,
@@ -300,33 +345,33 @@ describe('TeamEnvironmentsService', () => {
}); });
describe('createDuplicateEnvironment', () => { describe('createDuplicateEnvironment', () => {
test('should successfully duplicate an existing team environment', async () => { test('should duplicate an existing team environment', async () => {
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce( mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
teamEnvironment, teamEnvironment,
); );
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({ mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
id: 'newid',
...teamEnvironment, ...teamEnvironment,
id: 'newid',
}); });
const result = await teamEnvironmentsService.createDuplicateEnvironment( const result = await teamEnvironmentsService.createDuplicateEnvironment(
teamEnvironment.id, teamEnvironment.id,
); )();
expect(result).toEqualRight(<TeamEnvironment>{ expect(result).toEqualRight(<TeamEnvironment>{
id: 'newid',
...teamEnvironment, ...teamEnvironment,
id: 'newid',
variables: JSON.stringify(teamEnvironment.variables), variables: JSON.stringify(teamEnvironment.variables),
}); });
}); });
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError'); mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
const result = await teamEnvironmentsService.createDuplicateEnvironment( const result = await teamEnvironmentsService.createDuplicateEnvironment(
teamEnvironment.id, teamEnvironment.id,
); )();
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
}); });
@@ -337,19 +382,19 @@ 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(
teamEnvironment.id, teamEnvironment.id,
); )();
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_environment/${teamEnvironment.teamID}/created`, `team_environment/${teamEnvironment.teamID}/created`,
{ {
id: 'newid',
...teamEnvironment, ...teamEnvironment,
id: 'newid',
variables: JSON.stringify([{}]), variables: JSON.stringify([{}]),
}, },
); );

View File

@@ -1,14 +1,15 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TeamEnvironment as DBTeamEnvironment, Prisma } from '@prisma/client'; import { pipe } from 'fp-ts/function';
import * as T from 'fp-ts/Task';
import * as TO from 'fp-ts/TaskOption';
import * as TE from 'fp-ts/TaskEither';
import * as A from 'fp-ts/Array';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { TeamEnvironment } from './team-environments.model'; import { TeamEnvironment } from './team-environments.model';
import { import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
TEAM_ENVIRONMENT_NOT_FOUND,
TEAM_ENVIRONMENT_SHORT_NAME,
} from 'src/errors';
import * as E from 'fp-ts/Either';
import { isValidLength } from 'src/utils';
@Injectable() @Injectable()
export class TeamEnvironmentsService { export class TeamEnvironmentsService {
constructor( constructor(
@@ -16,218 +17,219 @@ export class TeamEnvironmentsService {
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
) {} ) {}
TITLE_LENGTH = 3; getTeamEnvironment(id: string) {
return TO.tryCatch(() =>
/** this.prisma.teamEnvironment.findFirst({
* TeamEnvironments are saved in the DB in the following way
* [{ key: value }, { key: value },....]
*
*/
/**
* Typecast a database TeamEnvironment to a TeamEnvironment model
* @param teamEnvironment database TeamEnvironment
* @returns TeamEnvironment model
*/
private cast(teamEnvironment: DBTeamEnvironment): TeamEnvironment {
return {
id: teamEnvironment.id,
name: teamEnvironment.name,
teamID: teamEnvironment.teamID,
variables: JSON.stringify(teamEnvironment.variables),
};
}
/**
* Get details of a TeamEnvironment.
*
* @param id TeamEnvironment ID
* @returns Either of a TeamEnvironment or error message
*/
async getTeamEnvironment(id: string) {
try {
const teamEnvironment =
await this.prisma.teamEnvironment.findFirstOrThrow({
where: { id }, where: { id },
}); rejectOnNotFound: true,
return E.right(teamEnvironment); }),
} catch (error) { );
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
} }
/** createTeamEnvironment(name: string, teamID: string, variables: string) {
* Create a new TeamEnvironment. return pipe(
* () =>
* @param name name of new TeamEnvironment this.prisma.teamEnvironment.create({
* @param teamID teamID of new TeamEnvironment
* @param variables JSONified string of contents of new TeamEnvironment
* @returns Either of a TeamEnvironment or error message
*/
async createTeamEnvironment(name: string, teamID: string, variables: string) {
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
const result = await this.prisma.teamEnvironment.create({
data: { data: {
name: name, name: name,
teamID: teamID, teamID: teamID,
variables: JSON.parse(variables), variables: JSON.parse(variables),
}, },
}); }),
T.chainFirst(
const createdTeamEnvironment = this.cast(result); (environment) => () =>
this.pubsub.publish( this.pubsub.publish(
`team_environment/${createdTeamEnvironment.teamID}/created`, `team_environment/${environment.teamID}/created`,
createdTeamEnvironment, <TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
T.map((data) => {
return <TeamEnvironment>{
id: data.id,
name: data.name,
teamID: data.teamID,
variables: JSON.stringify(data.variables),
};
}),
); );
return E.right(createdTeamEnvironment);
} }
/** deleteTeamEnvironment(id: string) {
* Delete a TeamEnvironment. return pipe(
* TE.tryCatch(
* @param id TeamEnvironment ID () =>
* @returns Either of boolean or error message this.prisma.teamEnvironment.delete({
*/
async deleteTeamEnvironment(id: string) {
try {
const result = await this.prisma.teamEnvironment.delete({
where: { where: {
id: id, id: id,
}, },
}); }),
() => TEAM_ENVIRONMENT_NOT_FOUND,
const deletedTeamEnvironment = this.cast(result); ),
TE.chainFirst((environment) =>
TE.fromTask(() =>
this.pubsub.publish( this.pubsub.publish(
`team_environment/${deletedTeamEnvironment.teamID}/deleted`, `team_environment/${environment.teamID}/deleted`,
deletedTeamEnvironment, <TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
),
TE.map((data) => true),
); );
return E.right(true);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
} }
/** updateTeamEnvironment(id: string, name: string, variables: string) {
* Update a TeamEnvironment. return pipe(
* TE.tryCatch(
* @param id TeamEnvironment ID () =>
* @param name TeamEnvironment name this.prisma.teamEnvironment.update({
* @param variables JSONified string of contents of new TeamEnvironment
* @returns Either of a TeamEnvironment or error message
*/
async updateTeamEnvironment(id: string, name: string, variables: string) {
try {
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
const result = await this.prisma.teamEnvironment.update({
where: { id: id }, where: { id: id },
data: { data: {
name, name,
variables: JSON.parse(variables), variables: JSON.parse(variables),
}, },
}); }),
() => TEAM_ENVIRONMENT_NOT_FOUND,
const updatedTeamEnvironment = this.cast(result); ),
TE.chainFirst((environment) =>
TE.fromTask(() =>
this.pubsub.publish( this.pubsub.publish(
`team_environment/${updatedTeamEnvironment.teamID}/updated`, `team_environment/${environment.teamID}/updated`,
updatedTeamEnvironment, <TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
),
TE.map(
(environment) =>
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
); );
return E.right(updatedTeamEnvironment);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
} }
/** deleteAllVariablesFromTeamEnvironment(id: string) {
* Clear contents of a TeamEnvironment. return pipe(
* TE.tryCatch(
* @param id TeamEnvironment ID () =>
* @returns Either of a TeamEnvironment or error message this.prisma.teamEnvironment.update({
*/
async deleteAllVariablesFromTeamEnvironment(id: string) {
try {
const result = await this.prisma.teamEnvironment.update({
where: { id: id }, where: { id: id },
data: { data: {
variables: [], variables: [],
}, },
}); }),
() => TEAM_ENVIRONMENT_NOT_FOUND,
const teamEnvironment = this.cast(result); ),
TE.chainFirst((environment) =>
TE.fromTask(() =>
this.pubsub.publish( this.pubsub.publish(
`team_environment/${teamEnvironment.teamID}/updated`, `team_environment/${environment.teamID}/updated`,
teamEnvironment, <TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
),
TE.map(
(environment) =>
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
); );
return E.right(teamEnvironment);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
} }
/** createDuplicateEnvironment(id: string) {
* Create a duplicate of a existing TeamEnvironment. return pipe(
* TE.tryCatch(
* @param id TeamEnvironment ID () =>
* @returns Either of a TeamEnvironment or error message this.prisma.teamEnvironment.findFirst({
*/
async createDuplicateEnvironment(id: string) {
try {
const environment = await this.prisma.teamEnvironment.findFirst({
where: { where: {
id: id, id: id,
}, },
rejectOnNotFound: true, rejectOnNotFound: true,
}); }),
() => TEAM_ENVIRONMENT_NOT_FOUND,
const result = await this.prisma.teamEnvironment.create({ ),
TE.chain((environment) =>
TE.fromTask(() =>
this.prisma.teamEnvironment.create({
data: { data: {
name: environment.name, name: environment.name,
teamID: environment.teamID, teamID: environment.teamID,
variables: environment.variables as Prisma.JsonArray, variables: environment.variables as Prisma.JsonArray,
}, },
}); }),
),
const duplicatedTeamEnvironment = this.cast(result); ),
TE.chainFirst((environment) =>
TE.fromTask(() =>
this.pubsub.publish( this.pubsub.publish(
`team_environment/${duplicatedTeamEnvironment.teamID}/created`, `team_environment/${environment.teamID}/created`,
duplicatedTeamEnvironment, <TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
),
TE.map(
(environment) =>
<TeamEnvironment>{
id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
); );
return E.right(duplicatedTeamEnvironment);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
} }
/** fetchAllTeamEnvironments(teamID: string) {
* Fetch all TeamEnvironments of a team. return pipe(
* () =>
* @param teamID teamID of new TeamEnvironment this.prisma.teamEnvironment.findMany({
* @returns List of TeamEnvironments
*/
async fetchAllTeamEnvironments(teamID: string) {
const result = await this.prisma.teamEnvironment.findMany({
where: { where: {
teamID: teamID, teamID: teamID,
}, },
}); }),
const teamEnvironments = result.map((item) => { T.map(
return this.cast(item); A.map(
}); (environment) =>
<TeamEnvironment>{
return teamEnvironments; id: environment.id,
name: environment.name,
teamID: environment.teamID,
variables: JSON.stringify(environment.variables),
},
),
),
);
} }
/** /**

View File

@@ -11,6 +11,6 @@ export class TeamEnvsTeamResolver {
description: 'Returns all Team Environments for the given Team', description: 'Returns all Team Environments for the given Team',
}) })
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> { teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id); return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
} }
} }

View File

@@ -1,20 +0,0 @@
import { ArgsType, Field, ID } from '@nestjs/graphql';
import { TeamMemberRole } from 'src/team/team.model';
@ArgsType()
export class CreateTeamInvitationArgs {
@Field(() => ID, {
name: 'teamID',
description: 'ID of the Team ID to invite from',
})
teamID: string;
@Field({ name: 'inviteeEmail', description: 'Email of the user to invite' })
inviteeEmail: string;
@Field(() => TeamMemberRole, {
name: 'inviteeRole',
description: 'Role to be given to the user',
})
inviteeRole: TeamMemberRole;
}

View File

@@ -12,10 +12,15 @@ import { TeamInvitation } from './team-invitation.model';
import { TeamInvitationService } from './team-invitation.service'; import { TeamInvitationService } from './team-invitation.service';
import { pipe } from 'fp-ts/function'; import { pipe } from 'fp-ts/function';
import * as TE from 'fp-ts/TaskEither'; import * as TE from 'fp-ts/TaskEither';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model'; import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors'; import { EmailCodec } from 'src/types/Email';
import {
INVALID_EMAIL,
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
TEAM_INVITE_NO_INVITE_FOUND,
USER_NOT_FOUND,
} from 'src/errors';
import { GqlUser } from 'src/decorators/gql-user.decorator'; import { GqlUser } from 'src/decorators/gql-user.decorator';
import { User } from 'src/user/user.model'; import { User } from 'src/user/user.model';
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
@@ -31,8 +36,6 @@ import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler'; import { SkipThrottle } from '@nestjs/throttler';
import { AuthUser } from 'src/types/AuthUser';
import { CreateTeamInvitationArgs } from './input-type.args';
@UseGuards(GqlThrottlerGuard) @UseGuards(GqlThrottlerGuard)
@Resolver(() => TeamInvitation) @Resolver(() => TeamInvitation)
@@ -76,8 +79,8 @@ export class TeamInvitationResolver {
'Gets the Team Invitation with the given ID, or null if not exists', 'Gets the Team Invitation with the given ID, or null if not exists',
}) })
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard) @UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
async teamInvitation( teamInvitation(
@GqlUser() user: AuthUser, @GqlUser() user: User,
@Args({ @Args({
name: 'inviteID', name: 'inviteID',
description: 'ID of the Team Invitation to lookup', description: 'ID of the Team Invitation to lookup',
@@ -85,11 +88,17 @@ export class TeamInvitationResolver {
}) })
inviteID: string, inviteID: string,
): Promise<TeamInvitation> { ): Promise<TeamInvitation> {
const teamInvitation = await this.teamInvitationService.getInvitation( return pipe(
inviteID, this.teamInvitationService.getInvitation(inviteID),
); TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
if (O.isNone(teamInvitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND); TE.chainW(
return teamInvitation.value; TE.fromPredicate(
(a) => a.inviteeEmail.toLowerCase() === user.email?.toLowerCase(),
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
),
),
TE.getOrElse(throwErr),
)();
} }
@Mutation(() => TeamInvitation, { @Mutation(() => TeamInvitation, {
@@ -97,19 +106,56 @@ export class TeamInvitationResolver {
}) })
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER) @RequiresTeamRole(TeamMemberRole.OWNER)
async createTeamInvitation( createTeamInvitation(
@GqlUser() user: AuthUser, @GqlUser()
@Args() args: CreateTeamInvitationArgs, user: User,
): Promise<TeamInvitation> {
const teamInvitation = await this.teamInvitationService.createInvitation(
user,
args.teamID,
args.inviteeEmail,
args.inviteeRole,
);
if (E.isLeft(teamInvitation)) throwErr(teamInvitation.left); @Args({
return teamInvitation.right; name: 'teamID',
description: 'ID of the Team ID to invite from',
type: () => ID,
})
teamID: string,
@Args({
name: 'inviteeEmail',
description: 'Email of the user to invite',
})
inviteeEmail: string,
@Args({
name: 'inviteeRole',
type: () => TeamMemberRole,
description: 'Role to be given to the user',
})
inviteeRole: TeamMemberRole,
): Promise<TeamInvitation> {
return pipe(
TE.Do,
// Validate email
TE.bindW('email', () =>
pipe(
EmailCodec.decode(inviteeEmail),
TE.fromEither,
TE.mapLeft(() => INVALID_EMAIL),
),
),
// Validate and get Team
TE.bindW('team', () => this.teamService.getTeamWithIDTE(teamID)),
// Create team
TE.chainW(({ email, team }) =>
this.teamInvitationService.createInvitation(
user,
team,
email,
inviteeRole,
),
),
// If failed, throw err (so the message is passed) else return value
TE.getOrElse(throwErr),
)();
} }
@Mutation(() => Boolean, { @Mutation(() => Boolean, {
@@ -117,7 +163,7 @@ export class TeamInvitationResolver {
}) })
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard) @UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
@RequiresTeamRole(TeamMemberRole.OWNER) @RequiresTeamRole(TeamMemberRole.OWNER)
async revokeTeamInvitation( revokeTeamInvitation(
@Args({ @Args({
name: 'inviteID', name: 'inviteID',
type: () => ID, type: () => ID,
@@ -125,19 +171,19 @@ export class TeamInvitationResolver {
}) })
inviteID: string, inviteID: string,
): Promise<true> { ): Promise<true> {
const isRevoked = await this.teamInvitationService.revokeInvitation( return pipe(
inviteID, this.teamInvitationService.revokeInvitation(inviteID),
); TE.map(() => true as const),
if (E.isLeft(isRevoked)) throwErr(isRevoked.left); TE.getOrElse(throwErr),
return true; )();
} }
@Mutation(() => TeamMember, { @Mutation(() => TeamMember, {
description: 'Accept an Invitation', description: 'Accept an Invitation',
}) })
@UseGuards(GqlAuthGuard, TeamInviteeGuard) @UseGuards(GqlAuthGuard, TeamInviteeGuard)
async acceptTeamInvitation( acceptTeamInvitation(
@GqlUser() user: AuthUser, @GqlUser() user: User,
@Args({ @Args({
name: 'inviteID', name: 'inviteID',
type: () => ID, type: () => ID,
@@ -145,12 +191,10 @@ export class TeamInvitationResolver {
}) })
inviteID: string, inviteID: string,
): Promise<TeamMember> { ): Promise<TeamMember> {
const teamMember = await this.teamInvitationService.acceptInvitation( return pipe(
inviteID, this.teamInvitationService.acceptInvitation(inviteID, user),
user, TE.getOrElse(throwErr),
); )();
if (E.isLeft(teamMember)) throwErr(teamMember.left);
return teamMember.right;
} }
// Subscriptions // Subscriptions

View File

@@ -1,25 +1,24 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as T from 'fp-ts/Task';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either'; import * as TO from 'fp-ts/TaskOption';
import * as TE from 'fp-ts/TaskEither';
import { pipe, flow, constVoid } from 'fp-ts/function';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { TeamInvitation as DBTeamInvitation } from '@prisma/client'; import { Team, TeamMemberRole } from 'src/team/team.model';
import { TeamMember, TeamMemberRole } from 'src/team/team.model'; import { Email } from 'src/types/Email';
import { User } from 'src/user/user.model';
import { TeamService } from 'src/team/team.service'; import { TeamService } from 'src/team/team.service';
import { import {
INVALID_EMAIL,
TEAM_INVALID_ID,
TEAM_INVITE_ALREADY_MEMBER, TEAM_INVITE_ALREADY_MEMBER,
TEAM_INVITE_EMAIL_DO_NOT_MATCH, TEAM_INVITE_EMAIL_DO_NOT_MATCH,
TEAM_INVITE_MEMBER_HAS_INVITE, TEAM_INVITE_MEMBER_HAS_INVITE,
TEAM_INVITE_NO_INVITE_FOUND, TEAM_INVITE_NO_INVITE_FOUND,
TEAM_MEMBER_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import { TeamInvitation } from './team-invitation.model'; import { TeamInvitation } from './team-invitation.model';
import { MailerService } from 'src/mailer/mailer.service'; import { MailerService } from 'src/mailer/mailer.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { validateEmail } from '../utils';
import { AuthUser } from 'src/types/AuthUser';
@Injectable() @Injectable()
export class TeamInvitationService { export class TeamInvitationService {
@@ -30,221 +29,245 @@ export class TeamInvitationService {
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
) {} ) {
this.getInvitation = this.getInvitation.bind(this);
/**
* Cast a DBTeamInvitation to a TeamInvitation
* @param dbTeamInvitation database TeamInvitation
* @returns TeamInvitation model
*/
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
return {
...dbTeamInvitation,
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
};
} }
/** getInvitation(inviteID: string): TO.TaskOption<TeamInvitation> {
* Get the team invite return pipe(
* @param inviteID invite id () =>
* @returns an Option of team invitation or none this.prisma.teamInvitation.findUnique({
*/
async getInvitation(inviteID: string) {
try {
const dbInvitation = await this.prisma.teamInvitation.findUniqueOrThrow({
where: { where: {
id: inviteID, id: inviteID,
}, },
}); }),
TO.fromTask,
return O.some(this.cast(dbInvitation)); TO.chain(flow(O.fromNullable, TO.fromOption)),
} catch (e) { TO.map((x) => x as TeamInvitation),
return O.none; );
}
} }
/** getInvitationWithEmail(email: Email, team: Team) {
* Get the team invite for an invitee with email and teamID. return pipe(
* @param inviteeEmail invitee email () =>
* @param teamID team id this.prisma.teamInvitation.findUnique({
* @returns an Either of team invitation for the invitee or error
*/
async getTeamInviteByEmailAndTeamID(inviteeEmail: string, teamID: string) {
const isEmailValid = validateEmail(inviteeEmail);
if (!isEmailValid) return E.left(INVALID_EMAIL);
try {
const teamInvite = await this.prisma.teamInvitation.findUniqueOrThrow({
where: { where: {
teamID_inviteeEmail: { teamID_inviteeEmail: {
inviteeEmail: inviteeEmail, inviteeEmail: email,
teamID: teamID, teamID: team.id,
}, },
}, },
}); }),
TO.fromTask,
return E.right(teamInvite); TO.chain(flow(O.fromNullable, TO.fromOption)),
} catch (e) { );
return E.left(TEAM_INVITE_NO_INVITE_FOUND);
}
} }
/** createInvitation(
* Create a team invitation creator: User,
* @param creator creator of the invitation team: Team,
* @param teamID team id inviteeEmail: Email,
* @param inviteeEmail invitee email
* @param inviteeRole invitee role
* @returns an Either of team invitation or error message
*/
async createInvitation(
creator: AuthUser,
teamID: string,
inviteeEmail: string,
inviteeRole: TeamMemberRole, inviteeRole: TeamMemberRole,
) { ) {
// validate email return pipe(
const isEmailValid = validateEmail(inviteeEmail); // Perform all validation checks
if (!isEmailValid) return E.left(INVALID_EMAIL); TE.sequenceArray([
// creator should be a TeamMember
pipe(
this.teamService.getTeamMemberTE(team.id, creator.uid),
TE.map(constVoid),
),
// team ID should valid // Invitee should not be a team member
const team = await this.teamService.getTeamWithID(teamID); pipe(
if (!team) return E.left(TEAM_INVALID_ID); async () => await this.userService.findUserByEmail(inviteeEmail),
TO.foldW(
() => TE.right(undefined), // If no user, short circuit to completion
(user) =>
pipe(
// If user is found, check if team member
this.teamService.getTeamMemberTE(team.id, user.uid),
TE.foldW(
() => TE.right(undefined), // Not team-member, this is good
() => TE.left(TEAM_INVITE_ALREADY_MEMBER), // Is team member, not good
),
),
),
TE.map(constVoid),
),
// invitation creator should be a TeamMember // Should not have an existing invite
const isTeamMember = await this.teamService.getTeamMember( pipe(
team.id, this.getInvitationWithEmail(inviteeEmail, team),
creator.uid, TE.fromTaskOption(() => null),
); TE.swap,
if (!isTeamMember) return E.left(TEAM_MEMBER_NOT_FOUND); TE.map(constVoid),
TE.mapLeft(() => TEAM_INVITE_MEMBER_HAS_INVITE),
),
]),
// Checking to see if the invitee is already part of the team or not // Create the invitation
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail); TE.chainTaskK(
if (O.isSome(inviteeUser)) { () => () =>
// invitee should not already a member this.prisma.teamInvitation.create({
const isTeamMember = await this.teamService.getTeamMember(
team.id,
inviteeUser.value.uid,
);
if (isTeamMember) return E.left(TEAM_INVITE_ALREADY_MEMBER);
}
// check invitee already invited earlier or not
const teamInvitation = await this.getTeamInviteByEmailAndTeamID(
inviteeEmail,
team.id,
);
if (E.isRight(teamInvitation)) return E.left(TEAM_INVITE_MEMBER_HAS_INVITE);
// create the invitation
const dbInvitation = await this.prisma.teamInvitation.create({
data: { data: {
teamID: team.id, teamID: team.id,
inviteeEmail, inviteeEmail,
inviteeRole, inviteeRole,
creatorUid: creator.uid, creatorUid: creator.uid,
}, },
}); }),
),
await this.mailerService.sendEmail(inviteeEmail, { // Send email, this is a side effect
TE.chainFirstTaskK((invitation) =>
pipe(
this.mailerService.sendMail(inviteeEmail, {
template: 'team-invitation', template: 'team-invitation',
variables: { variables: {
invitee: creator.displayName ?? 'A Hoppscotch User', invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`, action_url: `https://hoppscotch.io/join-team?id=${invitation.id}`,
invite_team_name: team.name, invite_team_name: team.name,
}, },
}); }),
const invitation = this.cast(dbInvitation); TE.getOrElseW(() => T.of(undefined)), // This value doesn't matter as we don't mind the return value (chainFirst) as long as the task completes
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation); ),
),
return E.right(invitation); // Send PubSub topic
TE.chainFirstTaskK((invitation) =>
TE.fromTask(async () => {
const inv: TeamInvitation = {
id: invitation.id,
teamID: invitation.teamID,
creatorUid: invitation.creatorUid,
inviteeEmail: invitation.inviteeEmail,
inviteeRole: TeamMemberRole[invitation.inviteeRole],
};
this.pubsub.publish(`team/${inv.teamID}/invite_added`, inv);
}),
),
// Map to model type
TE.map((x) => x as TeamInvitation),
);
} }
/** revokeInvitation(inviteID: string) {
* Revoke a team invitation return pipe(
* @param inviteID invite id // Make sure invite exists
* @returns an Either of true or error message this.getInvitation(inviteID),
*/ TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
async revokeInvitation(inviteID: string) {
// check if the invite exists
const invitation = await this.getInvitation(inviteID);
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
// delete the invite // Delete team invitation
await this.prisma.teamInvitation.delete({ TE.chainTaskK(
() => () =>
this.prisma.teamInvitation.delete({
where: { where: {
id: inviteID, id: inviteID,
}, },
}); }),
),
// Emit Pubsub Event
TE.chainFirst((invitation) =>
TE.fromTask(() =>
this.pubsub.publish( this.pubsub.publish(
`team/${invitation.value.teamID}/invite_removed`, `team/${invitation.teamID}/invite_removed`,
invitation.value.id, invitation.id,
); ),
),
),
return E.right(true); // We are not returning anything
TE.map(constVoid),
);
}
getAllInvitationsInTeam(team: Team) {
return pipe(
() =>
this.prisma.teamInvitation.findMany({
where: {
teamID: team.id,
},
}),
T.map((x) => x as TeamInvitation[]),
);
}
acceptInvitation(inviteID: string, acceptedBy: User) {
return pipe(
TE.Do,
// First get the invitation
TE.bindW('invitation', () =>
pipe(
this.getInvitation(inviteID),
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
),
),
// Validation checks
TE.chainFirstW(({ invitation }) =>
TE.sequenceArray([
// Make sure the invited user is not part of the team
pipe(
this.teamService.getTeamMemberTE(invitation.teamID, acceptedBy.uid),
TE.swap,
TE.bimap(
() => TEAM_INVITE_ALREADY_MEMBER,
constVoid, // The return type is ignored
),
),
// Make sure the invited user and accepting user has the same email
pipe(
undefined,
TE.fromPredicate(
(a) => acceptedBy.email === invitation.inviteeEmail,
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
),
),
]),
),
// Add the team member
// TODO: Somehow bring subscriptions to this ?
TE.bindW('teamMember', ({ invitation }) =>
pipe(
TE.tryCatch(
() =>
this.teamService.addMemberToTeam(
invitation.teamID,
acceptedBy.uid,
invitation.inviteeRole,
),
() => TEAM_INVITE_ALREADY_MEMBER, // Can only fail if Team Member already exists, which we checked, but due to async lets assert that here too
),
),
),
TE.chainFirstW(({ invitation }) => this.revokeInvitation(invitation.id)),
TE.map(({ teamMember }) => teamMember),
);
} }
/** /**
* Accept a team invitation * Fetch the count invitations for a given team.
* @param inviteID invite id
* @param acceptedBy user who accepted the invitation
* @returns an Either of team member or error message
*/
async acceptInvitation(inviteID: string, acceptedBy: AuthUser) {
// check if the invite exists
const invitation = await this.getInvitation(inviteID);
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
// make sure the user is not already a member of the team
const teamMemberInvitee = await this.teamService.getTeamMember(
invitation.value.teamID,
acceptedBy.uid,
);
if (teamMemberInvitee) return E.left(TEAM_INVITE_ALREADY_MEMBER);
// make sure the user is the same as the invitee
if (
acceptedBy.email.toLowerCase() !==
invitation.value.inviteeEmail.toLowerCase()
)
return E.left(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
// add the user to the team
let teamMember: TeamMember;
try {
teamMember = await this.teamService.addMemberToTeam(
invitation.value.teamID,
acceptedBy.uid,
invitation.value.inviteeRole,
);
} catch (e) {
return E.left(TEAM_INVITE_ALREADY_MEMBER);
}
// delete the invite
await this.revokeInvitation(inviteID);
return E.right(teamMember);
}
/**
* Fetch all team invitations for a given team.
* @param teamID team id * @param teamID team id
* @returns array of team invitations for a team * @returns a count team invitations for a team
*/ */
async getTeamInvitations(teamID: string) { async getAllTeamInvitations(teamID: string) {
const dbInvitations = await this.prisma.teamInvitation.findMany({ const invitations = await this.prisma.teamInvitation.findMany({
where: { where: {
teamID: teamID, teamID: teamID,
}, },
}); });
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
this.cast(dbInvitation),
);
return invitations; return invitations;
} }
} }

View File

@@ -1,21 +1,21 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { pipe } from 'fp-ts/function';
import { TeamService } from 'src/team/team.service'; import { TeamService } from 'src/team/team.service';
import { TeamInvitationService } from './team-invitation.service'; import { TeamInvitationService } from './team-invitation.service';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as T from 'fp-ts/Task';
import * as TE from 'fp-ts/TaskEither';
import { GqlExecutionContext } from '@nestjs/graphql'; import { GqlExecutionContext } from '@nestjs/graphql';
import { import {
BUG_AUTH_NO_USER_CTX, BUG_AUTH_NO_USER_CTX,
BUG_TEAM_INVITE_NO_INVITE_ID, BUG_TEAM_INVITE_NO_INVITE_ID,
TEAM_INVITE_NO_INVITE_FOUND, TEAM_INVITE_NO_INVITE_FOUND,
TEAM_MEMBER_NOT_FOUND,
TEAM_NOT_REQUIRED_ROLE, TEAM_NOT_REQUIRED_ROLE,
} from 'src/errors'; } from 'src/errors';
import { User } from 'src/user/user.model';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { TeamMemberRole } from 'src/team/team.model'; import { TeamMemberRole } from 'src/team/team.model';
/**
* This guard only allows team owner to execute the resolver
*/
@Injectable() @Injectable()
export class TeamInviteTeamOwnerGuard implements CanActivate { export class TeamInviteTeamOwnerGuard implements CanActivate {
constructor( constructor(
@@ -24,30 +24,48 @@ export class TeamInviteTeamOwnerGuard implements CanActivate {
) {} ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
// Get GQL context return pipe(
const gqlExecCtx = GqlExecutionContext.create(context); TE.Do,
// Get user TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
const { user } = gqlExecCtx.getContext().req;
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
// Get the invite // Get the invite
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>(); TE.bindW('invite', ({ gqlCtx }) =>
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID); pipe(
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
TE.chainW((inviteID) =>
pipe(
this.teamInviteService.getInvitation(inviteID),
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
),
),
),
),
const invitation = await this.teamInviteService.getInvitation(inviteID); TE.bindW('user', ({ gqlCtx }) =>
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND); pipe(
gqlCtx.getContext().req.user,
O.fromNullable,
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
),
),
// Fetch team member details of this user TE.bindW('userMember', ({ invite, user }) =>
const teamMember = await this.teamService.getTeamMember( this.teamService.getTeamMemberTE(invite.teamID, user.uid),
invitation.value.teamID, ),
user.uid,
);
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND); TE.chainW(
if (teamMember.role !== TeamMemberRole.OWNER) TE.fromPredicate(
throwErr(TEAM_NOT_REQUIRED_ROLE); ({ userMember }) => userMember.role === TeamMemberRole.OWNER,
() => TEAM_NOT_REQUIRED_ROLE,
),
),
return true; TE.fold(
(err) => throwErr(err),
() => T.of(true),
),
)();
} }
} }

View File

@@ -1,23 +1,20 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { TeamInvitationService } from './team-invitation.service'; import { TeamInvitationService } from './team-invitation.service';
import { pipe, flow } from 'fp-ts/function';
import * as TE from 'fp-ts/TaskEither';
import * as T from 'fp-ts/Task';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { GqlExecutionContext } from '@nestjs/graphql'; import { GqlExecutionContext } from '@nestjs/graphql';
import { import {
BUG_AUTH_NO_USER_CTX, BUG_AUTH_NO_USER_CTX,
BUG_TEAM_INVITE_NO_INVITE_ID, BUG_TEAM_INVITE_NO_INVITE_ID,
TEAM_INVITE_NOT_VALID_VIEWER,
TEAM_INVITE_NO_INVITE_FOUND, TEAM_INVITE_NO_INVITE_FOUND,
TEAM_MEMBER_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import { User } from 'src/user/user.model';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { TeamService } from 'src/team/team.service'; import { TeamService } from 'src/team/team.service';
/**
* This guard only allows user to execute the resolver
* 1. If user is invitee, allow
* 2. Or else, if user is team member, allow
*
* TLDR: Allow if user is invitee or team member
*/
@Injectable() @Injectable()
export class TeamInviteViewerGuard implements CanActivate { export class TeamInviteViewerGuard implements CanActivate {
constructor( constructor(
@@ -26,32 +23,50 @@ export class TeamInviteViewerGuard implements CanActivate {
) {} ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
// Get GQL context return pipe(
const gqlExecCtx = GqlExecutionContext.create(context); TE.Do,
// Get GQL Context
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
// Get user // Get user
const { user } = gqlExecCtx.getContext().req; TE.bindW('user', ({ gqlCtx }) =>
if (!user) throwErr(BUG_AUTH_NO_USER_CTX); pipe(
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
),
),
// Get the invite // Get the invite
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>(); TE.bindW('invite', ({ gqlCtx }) =>
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID); pipe(
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
TE.chainW(
flow(
this.teamInviteService.getInvitation,
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
),
),
),
),
const invitation = await this.teamInviteService.getInvitation(inviteID); // Check if the user and the invite email match, else if we can resolver the user as a team member
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND); // any better solution ?
TE.chainW(({ user, invite }) =>
user.email?.toLowerCase() === invite.inviteeEmail.toLowerCase()
? TE.of(true)
: pipe(
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
TE.map(() => true),
),
),
// Check if the user and the invite email match, else if user is a team member TE.mapLeft((e) =>
if ( e === 'team/member_not_found' ? TEAM_INVITE_NOT_VALID_VIEWER : e,
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase() ),
) {
const teamMember = await this.teamService.getTeamMember(
invitation.value.teamID,
user.uid,
);
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND); TE.fold(throwErr, () => T.of(true)),
} )();
return true;
} }
} }

View File

@@ -1,7 +1,11 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { TeamInvitationService } from './team-invitation.service'; import { TeamInvitationService } from './team-invitation.service';
import { pipe, flow } from 'fp-ts/function';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as T from 'fp-ts/Task';
import * as TE from 'fp-ts/TaskEither';
import { GqlExecutionContext } from '@nestjs/graphql'; import { GqlExecutionContext } from '@nestjs/graphql';
import { User } from 'src/user/user.model';
import { import {
BUG_AUTH_NO_USER_CTX, BUG_AUTH_NO_USER_CTX,
BUG_TEAM_INVITE_NO_INVITE_ID, BUG_TEAM_INVITE_NO_INVITE_ID,
@@ -20,26 +24,44 @@ export class TeamInviteeGuard implements CanActivate {
constructor(private readonly teamInviteService: TeamInvitationService) {} constructor(private readonly teamInviteService: TeamInvitationService) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
// Get GQL Context return pipe(
const gqlExecCtx = GqlExecutionContext.create(context); TE.Do,
// Get execution context
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
// Get user // Get user
const { user } = gqlExecCtx.getContext().req; TE.bindW('user', ({ gqlCtx }) =>
if (!user) throwErr(BUG_AUTH_NO_USER_CTX); pipe(
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
),
),
// Get the invite // Get invite
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>(); TE.bindW('invite', ({ gqlCtx }) =>
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID); pipe(
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
TE.chainW(
flow(
this.teamInviteService.getInvitation,
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
),
),
),
),
const invitation = await this.teamInviteService.getInvitation(inviteID); // Check if the emails match
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND); TE.chainW(
TE.fromPredicate(
({ user, invite }) => user.email === invite.inviteeEmail,
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
),
),
if ( // Fold it to a promise
user.email.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase() TE.fold(throwErr, () => T.of(true)),
) { )();
throwErr(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
}
return true;
} }
} }

View File

@@ -12,6 +12,6 @@ export class TeamTeamInviteExtResolver {
complexity: 10, complexity: 10,
}) })
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> { teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
return this.teamInviteService.getTeamInvitations(team.id); return this.teamInviteService.getAllInvitationsInTeam(team)();
} }
} }

View File

@@ -2,7 +2,7 @@ import { HttpStatus } from '@nestjs/common';
/** /**
** Custom interface to handle errors specific to Auth module ** Custom interface to handle errors specific to Auth module
** Since its REST we need to return the HTTP status code along with the error message ** Since its REST we need to return HTTP status code along with error message
*/ */
export type AuthError = { export type AuthError = {
message: string; message: string;

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>

Before

Width:  |  Height:  |  Size: 337 B

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Reaksie liggaam", "body": "Reaksie liggaam",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Opskrifte", "headers": "Opskrifte",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Tyd", "time": "Tyd",
"title": "Reaksie", "title": "Reaksie",
"video": "Video",
"waiting_for_connection": "wag vir verbinding", "waiting_for_connection": "wag vir verbinding",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "هيئة الاستجابة", "body": "هيئة الاستجابة",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "الرؤوس", "headers": "الرؤوس",
@@ -446,7 +445,6 @@
"status": "حالة", "status": "حالة",
"time": "وقت", "time": "وقت",
"title": "إجابة", "title": "إجابة",
"video": "Video",
"waiting_for_connection": "في انتظار الاتصال", "waiting_for_connection": "في انتظار الاتصال",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "Visualitzar els meus enllaços" "view_my_links": "Visualitzar els meus enllaços"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Cos de resposta", "body": "Cos de resposta",
"filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)", "filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)",
"headers": "Capçaleres", "headers": "Capçaleres",
@@ -446,7 +445,6 @@
"status": "Estat", "status": "Estat",
"time": "Temps", "time": "Temps",
"title": "Resposta", "title": "Resposta",
"video": "Video",
"waiting_for_connection": "esperant la connexió", "waiting_for_connection": "esperant la connexió",
"xml": "XML" "xml": "XML"
}, },

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": "了解更多",
@@ -40,9 +40,9 @@
"start": "开始", "start": "开始",
"starting": "正在开始", "starting": "正在开始",
"stop": "停止", "stop": "停止",
"to_close": "关闭", "to_close": "关闭",
"to_navigate": "定位", "to_navigate": "定位",
"to_select": "选择", "to_select": "选择",
"turn_off": "关闭", "turn_off": "关闭",
"turn_on": "开启", "turn_on": "开启",
"undo": "撤消", "undo": "撤消",
@@ -118,16 +118,16 @@
}, },
"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": "另存为",
@@ -147,7 +147,7 @@
"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": {
@@ -177,7 +177,7 @@
"members": "团队为空", "members": "团队为空",
"parameters": "该请求没有任何参数", "parameters": "该请求没有任何参数",
"pending_invites": "此团队无待办邀请", "pending_invites": "此团队无待办邀请",
"profile": "登录以查看你的个人资料", "profile": "登录以查看你的个人档案",
"protocols": "协议为空", "protocols": "协议为空",
"schema": "连接至 GraphQL 端点", "schema": "连接至 GraphQL 端点",
"shortcodes": "Shortcodes 为空", "shortcodes": "Shortcodes 为空",
@@ -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": "空请求名称",
@@ -219,7 +219,7 @@
"incorrect_email": "电子邮箱错误", "incorrect_email": "电子邮箱错误",
"invalid_link": "无效链接", "invalid_link": "无效链接",
"invalid_link_description": "你点击的链接无效或已过期。", "invalid_link_description": "你点击的链接无效或已过期。",
"json_parsing_failed": "不合法的 JSON", "json_parsing_failed": "Invalid JSON",
"json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试", "json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试",
"network_error": "好像发生了网络错误,请重试。", "network_error": "好像发生了网络错误,请重试。",
"network_fail": "无法发送请求", "network_fail": "无法发送请求",
@@ -316,14 +316,14 @@
"zen_mode": "ZEN 模式" "zen_mode": "ZEN 模式"
}, },
"modal": { "modal": {
"close_unsaved_tab": "有未保存的变更", "close_unsaved_tab": "You have unsaved changes",
"collections": "集合", "collections": "集合",
"confirm": "确认", "confirm": "确认",
"edit_request": "编辑请求", "edit_request": "编辑请求",
"import_export": "导入/导出" "import_export": "导入/导出"
}, },
"mqtt": { "mqtt": {
"already_subscribed": "您已经订阅了此主。", "already_subscribed": "您已经订阅了此主。",
"clean_session": "清除会话", "clean_session": "清除会话",
"clear_input": "清除输入", "clear_input": "清除输入",
"clear_input_on_send": "发送后清除输入", "clear_input_on_send": "发送后清除输入",
@@ -355,7 +355,7 @@
"navigation": { "navigation": {
"doc": "文档", "doc": "文档",
"graphql": "GraphQL", "graphql": "GraphQL",
"profile": "个人资料", "profile": "个人档案",
"realtime": "实时", "realtime": "实时",
"rest": "REST", "rest": "REST",
"settings": "设置" "settings": "设置"
@@ -377,7 +377,7 @@
"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,6 @@
"view_my_links": "查看我的链接" "view_my_links": "查看我的链接"
}, },
"response": { "response": {
"audio": "Audio",
"body": "响应体", "body": "响应体",
"filter_response_body": "筛选JSON响应本体使用JSONPath语法", "filter_response_body": "筛选JSON响应本体使用JSONPath语法",
"headers": "响应头", "headers": "响应头",
@@ -446,7 +445,6 @@
"status": "状态", "status": "状态",
"time": "时间", "time": "时间",
"title": "响应", "title": "响应",
"video": "Video",
"waiting_for_connection": "等待连接", "waiting_for_connection": "等待连接",
"xml": "XML" "xml": "XML"
}, },
@@ -481,10 +479,10 @@
"language": "语言", "language": "语言",
"light_mode": "亮色", "light_mode": "亮色",
"official_proxy_hosting": "官方代理由 Hoppscotch 托管。", "official_proxy_hosting": "官方代理由 Hoppscotch 托管。",
"profile": "个人资料", "profile": "个人档案",
"profile_description": "更新你的资料", "profile_description": "更新你的档案详情",
"profile_email": "电子邮箱地址", "profile_email": "电子邮箱地址",
"profile_name": "名称", "profile_name": "档案名称",
"proxy": "网络代理", "proxy": "网络代理",
"proxy_url": "代理网址", "proxy_url": "代理网址",
"proxy_use_toggle": "使用代理中间件发送请求", "proxy_use_toggle": "使用代理中间件发送请求",
@@ -534,7 +532,7 @@
"documentation": "前往文档页面", "documentation": "前往文档页面",
"forward": "前往下一页面", "forward": "前往下一页面",
"graphql": "前往 GraphQL 页面", "graphql": "前往 GraphQL 页面",
"profile": "前往个人资料页面", "profile": "前往个人档案页面",
"realtime": "前往实时页面", "realtime": "前往实时页面",
"rest": "前往 REST 页面", "rest": "前往 REST 页面",
"settings": "前往设置页面", "settings": "前往设置页面",
@@ -576,7 +574,7 @@
}, },
"socketio": { "socketio": {
"communication": "通讯", "communication": "通讯",
"connection_not_authorized": "此 SocketIO 连接未使用任何验证。", "connection_not_authorized": "此SocketIO连接未使用任何验证。",
"event_name": "事件名称", "event_name": "事件名称",
"events": "事件", "events": "事件",
"log": "日志", "log": "日志",
@@ -616,12 +614,12 @@
"none": "无", "none": "无",
"nothing_found": "没有找到", "nothing_found": "没有找到",
"published_error": "将信息:{topic}发布至主题:{message}时发生错误", "published_error": "将信息:{topic}发布至主题:{message}时发生错误",
"published_message": "已将此信息:{message} 发布至主题:{topic}", "published_message": "已将此信息:{message}发布至主题:{topic}",
"reconnection_error": "重连失败", "reconnection_error": "重连失败",
"subscribed_failed": "无法订阅此主{topic}", "subscribed_failed": "无法订阅此主{topic}",
"subscribed_success": "成功订阅此主{topic}", "subscribed_success": "成功订阅此主{topic}",
"unsubscribed_failed": "无法取消订阅此主{topic}", "unsubscribed_failed": "无法取消订阅此主{topic}",
"unsubscribed_success": "成功取消订阅此主{topic}", "unsubscribed_success": "成功取消订阅此主{topic}",
"waiting_send_request": "等待发送请求" "waiting_send_request": "等待发送请求"
}, },
"support": { "support": {
@@ -641,7 +639,7 @@
"body": "请求体", "body": "请求体",
"collections": "集合", "collections": "集合",
"documentation": "帮助文档", "documentation": "帮助文档",
"environments": "环境", "environments": "Environments",
"headers": "请求头", "headers": "请求头",
"history": "历史记录", "history": "历史记录",
"mqtt": "MQTT", "mqtt": "MQTT",
@@ -666,7 +664,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,7 +688,7 @@
"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": "新团队",
@@ -698,13 +696,13 @@
"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 +732,9 @@
"url": "URL" "url": "URL"
}, },
"workspace": { "workspace": {
"change": "切换工作空间", "change": "Change workspace",
"personal": "我的工作空间", "personal": "My Workspace",
"team": "团队工作空间", "team": "Team Workspace",
"title": "工作空间" "title": "Workspaces"
} }
} }

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Odpovědní orgán", "body": "Odpovědní orgán",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Záhlaví", "headers": "Záhlaví",
@@ -446,7 +445,6 @@
"status": "Postavení", "status": "Postavení",
"time": "Čas", "time": "Čas",
"title": "Odezva", "title": "Odezva",
"video": "Video",
"waiting_for_connection": "čekání na připojení", "waiting_for_connection": "čekání na připojení",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Svarorgan", "body": "Svarorgan",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Overskrifter", "headers": "Overskrifter",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Tid", "time": "Tid",
"title": "Respons", "title": "Respons",
"video": "Video",
"waiting_for_connection": "venter på forbindelse", "waiting_for_connection": "venter på forbindelse",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Antworttext", "body": "Antworttext",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Header", "headers": "Header",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Zeit", "time": "Zeit",
"title": "Antwort", "title": "Antwort",
"video": "Video",
"waiting_for_connection": "auf Verbindung warten", "waiting_for_connection": "auf Verbindung warten",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "Προβολή των links μου" "view_my_links": "Προβολή των links μου"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Σώμα απόκρισης", "body": "Σώμα απόκρισης",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Κεφαλίδες", "headers": "Κεφαλίδες",
@@ -446,7 +445,6 @@
"status": "Κατάσταση", "status": "Κατάσταση",
"time": "χρόνος", "time": "χρόνος",
"title": "Απάντηση", "title": "Απάντηση",
"video": "Video",
"waiting_for_connection": "περιμένοντας τη σύνδεση", "waiting_for_connection": "περιμένοντας τη σύνδεση",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "Ver mis enlaces" "view_my_links": "Ver mis enlaces"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Cuerpo de respuesta", "body": "Cuerpo de respuesta",
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)", "filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
"headers": "Encabezados", "headers": "Encabezados",
@@ -446,7 +445,6 @@
"status": "Estado", "status": "Estado",
"time": "Tiempo", "time": "Tiempo",
"title": "Respuesta", "title": "Respuesta",
"video": "Video",
"waiting_for_connection": "esperando la conexión", "waiting_for_connection": "esperando la conexión",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Vastauselin", "body": "Vastauselin",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Otsikot", "headers": "Otsikot",
@@ -446,7 +445,6 @@
"status": "Tila", "status": "Tila",
"time": "Aika", "time": "Aika",
"title": "Vastaus", "title": "Vastaus",
"video": "Video",
"waiting_for_connection": "yhteyttä odotellessa", "waiting_for_connection": "yhteyttä odotellessa",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "Voir mes liens" "view_my_links": "Voir mes liens"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corps de réponse", "body": "Corps de réponse",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "En-têtes", "headers": "En-têtes",
@@ -446,7 +445,6 @@
"status": "Statut", "status": "Statut",
"time": "Temps", "time": "Temps",
"title": "Réponse", "title": "Réponse",
"video": "Video",
"waiting_for_connection": "En attente de connexion", "waiting_for_connection": "En attente de connexion",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "גוף תגובה", "body": "גוף תגובה",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "כותרות", "headers": "כותרות",
@@ -446,7 +445,6 @@
"status": "סטָטוּס", "status": "סטָטוּס",
"time": "זְמַן", "time": "זְמַן",
"title": "תְגוּבָה", "title": "תְגוּבָה",
"video": "Video",
"waiting_for_connection": "מחכה לחיבור", "waiting_for_connection": "מחכה לחיבור",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -433,7 +433,6 @@
"view_my_links": "मेरे लिंक देखें" "view_my_links": "मेरे लिंक देखें"
}, },
"response": { "response": {
"audio": "Audio",
"body": "प्रतिक्रिया निकाय", "body": "प्रतिक्रिया निकाय",
"filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)", "filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)",
"headers": "हेडर", "headers": "हेडर",
@@ -447,7 +446,6 @@
"status": "दर्जा", "status": "दर्जा",
"time": "समय", "time": "समय",
"title": "जवाब", "title": "जवाब",
"video": "Video",
"waiting_for_connection": "जुडने के लिए इंतजार", "waiting_for_connection": "जुडने के लिए इंतजार",
"xml": "एक्सएमएल" "xml": "एक्सएमएल"
}, },

View File

@@ -5,29 +5,29 @@
"choose_file": "Válasszon egy fájlt", "choose_file": "Válasszon egy fájlt",
"clear": "Törlés", "clear": "Törlés",
"clear_all": "Összes törlése", "clear_all": "Összes törlése",
"close": "Bezárás", "close": "Close",
"connect": "Kapcsolódás", "connect": "Kapcsolódás",
"connecting": "Kapcsolódás", "connecting": "Connecting",
"copy": "Másolás", "copy": "Másolás",
"delete": "Törlés", "delete": "Törlés",
"disconnect": "Leválasztás", "disconnect": "Leválasztás",
"dismiss": "Eltüntetés", "dismiss": "Eltüntetés",
"dont_save": "Ne mentse", "dont_save": "Ne mentse",
"download_file": "Fájl letöltése", "download_file": "Fájl letöltése",
"drag_to_reorder": "Húzza az átrendezéshez", "drag_to_reorder": "Drag to reorder",
"duplicate": "Kettőzés", "duplicate": "Kettőzés",
"edit": "Szerkesztés", "edit": "Szerkesztés",
"filter": "Szűrő", "filter": "Filter",
"go_back": "Vissza", "go_back": "Vissza",
"go_forward": "Előre", "go_forward": "Go forward",
"group_by": "Csoportosítás", "group_by": "Group by",
"label": "Címke", "label": "Címke",
"learn_more": "Tudjon meg többet", "learn_more": "Tudjon meg többet",
"less": "Kevesebb", "less": "Kevesebb",
"more": "Több", "more": "Több",
"new": "Új", "new": "Új",
"no": "Nem", "no": "Nem",
"open_workspace": "Munkaterület megnyitása", "open_workspace": "Open workspace",
"paste": "Beillesztés", "paste": "Beillesztés",
"prettify": "Csinosítás", "prettify": "Csinosítás",
"remove": "Eltávolítás", "remove": "Eltávolítás",
@@ -38,7 +38,7 @@
"search": "Keresés", "search": "Keresés",
"send": "Küldés", "send": "Küldés",
"start": "Indítás", "start": "Indítás",
"starting": "Indítás", "starting": "Starting",
"stop": "Leállítás", "stop": "Leállítás",
"to_close": "a bezáráshoz", "to_close": "a bezáráshoz",
"to_navigate": "a navigáláshoz", "to_navigate": "a navigáláshoz",
@@ -118,16 +118,16 @@
}, },
"collection": { "collection": {
"created": "Gyűjtemény létrehozva", "created": "Gyűjtemény létrehozva",
"different_parent": "Nem lehet átrendezni a különböző szülővel rendelkező gyűjteményt", "different_parent": "Cannot reorder collection with different parent",
"edit": "Gyűjtemény szerkesztése", "edit": "Gyűjtemény szerkesztése",
"invalid_name": "Adjon nevet a gyűjteménynek", "invalid_name": "Adjon nevet a gyűjteménynek",
"invalid_root_move": "A gyűjtemény már a gyökérben van", "invalid_root_move": "Collection already in the root",
"moved": "Sikeresen áthelyezve", "moved": "Moved Successfully",
"my_collections": "Saját gyűjtemények", "my_collections": "Saját gyűjtemények",
"name": "Saját új gyűjtemény", "name": "Saját új gyűjtemény",
"name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie", "name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie",
"new": "Új gyűjtemény", "new": "Új gyűjtemény",
"order_changed": "Gyűjtemény sorrendje frissítve", "order_changed": "Collection Order Updated",
"renamed": "Gyűjtemény átnevezve", "renamed": "Gyűjtemény átnevezve",
"request_in_use": "A kérés használatban", "request_in_use": "A kérés használatban",
"save_as": "Mentés másként", "save_as": "Mentés másként",
@@ -147,7 +147,7 @@
"remove_team": "Biztosan törölni szeretné ezt a csapatot?", "remove_team": "Biztosan törölni szeretné ezt a csapatot?",
"remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?", "remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?",
"request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.", "request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.",
"save_unsaved_tab": "Szeretné menteni az ezen a lapon elvégzett változtatásokat?", "save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát." "sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát."
}, },
"count": { "count": {
@@ -180,8 +180,8 @@
"profile": "Jelentkezzen be a profilja megtekintéséhez", "profile": "Jelentkezzen be a profilja megtekintéséhez",
"protocols": "A protokollok üresek", "protocols": "A protokollok üresek",
"schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez", "schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez",
"shortcodes": "A rövid kódok üresek", "shortcodes": "Shortcodes are empty",
"subscription": "A feliratkozások üresek", "subscription": "Subscriptions are empty",
"team_name": "A csapat neve üres", "team_name": "A csapat neve üres",
"teams": "Ön nem tartozik semmilyen csapathoz", "teams": "Ön nem tartozik semmilyen csapathoz",
"tests": "Nincsenek tesztek ehhez a kéréshez" "tests": "Nincsenek tesztek ehhez a kéréshez"
@@ -194,13 +194,13 @@
"deleted": "Környezet törlése", "deleted": "Környezet törlése",
"edit": "Környezet szerkesztése", "edit": "Környezet szerkesztése",
"invalid_name": "Adjon nevet a környezetnek", "invalid_name": "Adjon nevet a környezetnek",
"my_environments": "Saját környezetek", "my_environments": "My Environments",
"nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva", "nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva",
"new": "Új környezet", "new": "Új környezet",
"no_environment": "Nincs környezet", "no_environment": "Nincs környezet",
"no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.", "no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.",
"select": "Környezet kiválasztása", "select": "Környezet kiválasztása",
"team_environments": "Csapatkörnyezetek", "team_environments": "Team Environments",
"title": "Környezetek", "title": "Környezetek",
"updated": "Környezet frissítve", "updated": "Környezet frissítve",
"variable_list": "Változólista" "variable_list": "Változólista"
@@ -209,9 +209,9 @@
"browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.", "browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.",
"check_console_details": "Nézze meg a konzolnaplót a részletekért.", "check_console_details": "Nézze meg a konzolnaplót a részletekért.",
"curl_invalid_format": "A cURL nincs megfelelően formázva", "curl_invalid_format": "A cURL nincs megfelelően formázva",
"danger_zone": "Veszélyes zóna", "danger_zone": "Danger zone",
"delete_account": "Az Ön fiókja jelenleg tulajdonos ezekben a csapatokban:", "delete_account": "Your account is currently an owner in these teams:",
"delete_account_description": "El kell távolítani magát, át kell adnia a tulajdonjogot vagy törölnie kell ezeket a csapatokat, mielőtt törölhetné a fiókját.", "delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
"empty_req_name": "Üres kérésnév", "empty_req_name": "Üres kérésnév",
"f12_details": "(F12 a részletekért)", "f12_details": "(F12 a részletekért)",
"gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra", "gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra",
@@ -219,13 +219,13 @@
"incorrect_email": "Hibás e-mail", "incorrect_email": "Hibás e-mail",
"invalid_link": "Érvénytelen hivatkozás", "invalid_link": "Érvénytelen hivatkozás",
"invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.", "invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.",
"json_parsing_failed": "Érvénytelen JSON", "json_parsing_failed": "Invalid JSON",
"json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra", "json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra",
"network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.", "network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.",
"network_fail": "Nem sikerült elküldeni a kérést", "network_fail": "Nem sikerült elküldeni a kérést",
"no_duration": "Nincs időtartam", "no_duration": "Nincs időtartam",
"no_results_found": "Nincs találat", "no_results_found": "No matches found",
"page_not_found": "Ez az oldal nem található", "page_not_found": "This page could not be found",
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt", "script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
"something_went_wrong": "Valami elromlott", "something_went_wrong": "Valami elromlott",
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt" "test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
@@ -238,9 +238,9 @@
"title": "Exportálás" "title": "Exportálás"
}, },
"filter": { "filter": {
"all": "Összes", "all": "All",
"none": "Nincs", "none": "None",
"starred": "Csillagozott" "starred": "Starred"
}, },
"folder": { "folder": {
"created": "Mappa létrehozva", "created": "Mappa létrehozva",
@@ -256,7 +256,7 @@
"subscriptions": "Feliratkozások" "subscriptions": "Feliratkozások"
}, },
"group": { "group": {
"time": "Idő", "time": "Time",
"url": "URL" "url": "URL"
}, },
"header": { "header": {
@@ -316,32 +316,32 @@
"zen_mode": "Zen mód" "zen_mode": "Zen mód"
}, },
"modal": { "modal": {
"close_unsaved_tab": "Elmentetlen változtatásai vannak", "close_unsaved_tab": "You have unsaved changes",
"collections": "Gyűjtemények", "collections": "Gyűjtemények",
"confirm": "Megerősítés", "confirm": "Megerősítés",
"edit_request": "Kérés szerkesztése", "edit_request": "Kérés szerkesztése",
"import_export": "Importálás és exportálás" "import_export": "Importálás és exportálás"
}, },
"mqtt": { "mqtt": {
"already_subscribed": "Ön már feliratkozott erre a témára.", "already_subscribed": "You are already subscribed to this topic.",
"clean_session": "Munkamenet törlése", "clean_session": "Clean Session",
"clear_input": "Bevitel törlése", "clear_input": "Clear input",
"clear_input_on_send": "Bevitel törlése küldéskor", "clear_input_on_send": "Clear input on send",
"client_id": "Ügyfél-azonosító", "client_id": "Client ID",
"color": "Válasszon színt", "color": "Pick a color",
"communication": "Kommunikáció", "communication": "Kommunikáció",
"connection_config": "Kapcsolat beállításai", "connection_config": "Connection Config",
"connection_not_authorized": "Ez az MQTT-kapcsolat nem használ semmilyen hitelesítést.", "connection_not_authorized": "This MQTT connection does not use any authentication.",
"invalid_topic": "Adjon témát a feliratkozáshoz", "invalid_topic": "Please provide a topic for the subscription",
"keep_alive": "Életben tartás", "keep_alive": "Keep Alive",
"log": "Napló", "log": "Napló",
"lw_message": "Utolsó kívánság üzenet", "lw_message": "Last-Will Message",
"lw_qos": "Utolsó kívánság QoS", "lw_qos": "Last-Will QoS",
"lw_retain": "Utolsó kívánság megtartás", "lw_retain": "Last-Will Retain",
"lw_topic": "Utolsó kívánság téma", "lw_topic": "Last-Will Topic",
"message": "Üzenet", "message": "Üzenet",
"new": "Új feliratkozás", "new": "New Subscription",
"not_connected": "Először indítson egy MQTT-kapcsolatot.", "not_connected": "Please start a MQTT connection first.",
"publish": "Közzététel", "publish": "Közzététel",
"qos": "QoS", "qos": "QoS",
"ssl": "SSL", "ssl": "SSL",
@@ -368,7 +368,7 @@
}, },
"profile": { "profile": {
"app_settings": "Alkalmazás beállításai", "app_settings": "Alkalmazás beállításai",
"default_hopp_displayname": "Névtelen felhasználó", "default_hopp_displayname": "Unnamed User",
"editor": "Szerkesztő", "editor": "Szerkesztő",
"editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.", "editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.",
"email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.", "email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.",
@@ -391,26 +391,26 @@
"choose_language": "Nyelv kiválasztása", "choose_language": "Nyelv kiválasztása",
"content_type": "Tartalom típusa", "content_type": "Tartalom típusa",
"content_type_titles": { "content_type_titles": {
"others": "Egyebek", "others": "Others",
"structured": "Szerkesztett", "structured": "Structured",
"text": "Szöveg" "text": "Text"
}, },
"copy_link": "Hivatkozás másolása", "copy_link": "Hivatkozás másolása",
"different_collection": "Nem lehet átrendezni a különböző gyűjteményekből érkező kéréseket", "different_collection": "Cannot reorder requests from different collections",
"duplicated": "Kérés megkettőzve", "duplicated": "Request duplicated",
"duration": "Időtartam", "duration": "Időtartam",
"enter_curl": "cURL-parancs megadása", "enter_curl": "cURL megadása",
"generate_code": "Kód előállítása", "generate_code": "Kód előállítása",
"generated_code": "Előállított kód", "generated_code": "Előállított kód",
"header_list": "Fejléclista", "header_list": "Fejléclista",
"invalid_name": "Adjon nevet a kérésnek", "invalid_name": "Adjon nevet a kérésnek",
"method": "Módszer", "method": "Módszer",
"moved": "Kérés áthelyezve", "moved": "Request moved",
"name": "Kérés neve", "name": "Kérés neve",
"new": "Új kérés", "new": "Új kérés",
"order_changed": "Kérés sorrendje frissítve", "order_changed": "Request Order Updated",
"override": "Felülbírálás", "override": "Felülbírálás",
"override_help": "<kbd>Content-Type</kbd> beállítása a fejlécekben", "override_help": "A <kbd>Content-Type</kbd> beállítása a fejlécekben",
"overriden": "Felülbírálva", "overriden": "Felülbírálva",
"parameter_list": "Lekérdezési paraméterek", "parameter_list": "Lekérdezési paraméterek",
"parameters": "Paraméterek", "parameters": "Paraméterek",
@@ -429,12 +429,11 @@
"type": "Kérés típusa", "type": "Kérés típusa",
"url": "URL", "url": "URL",
"variables": "Változók", "variables": "Változók",
"view_my_links": "Saját hivatkozások megtekintése" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Hang",
"body": "Válasz törzse", "body": "Válasz törzse",
"filter_response_body": "JSON-válasz törzsének szűrése (JSONPath szintaxist használ)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Fejlécek", "headers": "Fejlécek",
"html": "HTML", "html": "HTML",
"image": "Kép", "image": "Kép",
@@ -446,14 +445,13 @@
"status": "Állapot", "status": "Állapot",
"time": "Idő", "time": "Idő",
"title": "Válasz", "title": "Válasz",
"video": "Videó",
"waiting_for_connection": "várakozás kapcsolódásra", "waiting_for_connection": "várakozás kapcsolódásra",
"xml": "XML" "xml": "XML"
}, },
"settings": { "settings": {
"accent_color": "Kiemelőszín", "accent_color": "Kiemelőszín",
"account": "Fiók", "account": "Fiók",
"account_deleted": "A fiókja törölve lett", "account_deleted": "Your account has been deleted",
"account_description": "A fiókbeállítások személyre szabása.", "account_description": "A fiókbeállítások személyre szabása.",
"account_email_description": "Az Ön elsődleges e-mail-címe.", "account_email_description": "Az Ön elsődleges e-mail-címe.",
"account_name_description": "Ez a megjelenített neve.", "account_name_description": "Ez a megjelenített neve.",
@@ -462,8 +460,8 @@
"change_font_size": "Betűméret megváltoztatása", "change_font_size": "Betűméret megváltoztatása",
"choose_language": "Nyelv kiválasztása", "choose_language": "Nyelv kiválasztása",
"dark_mode": "Sötét", "dark_mode": "Sötét",
"delete_account": "Fiók törlése", "delete_account": "Delete account",
"delete_account_description": "Ha törli a fiókját, akkor az összes adata véglegesen törlésre kerül. Ezt a műveletet nem lehet visszavonni.", "delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"expand_navigation": "Navigáció kinyitása", "expand_navigation": "Navigáció kinyitása",
"experiments": "Kísérletek", "experiments": "Kísérletek",
"experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ", "experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ",
@@ -490,8 +488,8 @@
"proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez", "proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez",
"read_the": "Olvassa el:", "read_the": "Olvassa el:",
"reset_default": "Visszaállítás az alapértelmezettre", "reset_default": "Visszaállítás az alapértelmezettre",
"short_codes": "Rövid kódok", "short_codes": "Short codes",
"short_codes_description": "Az Ön által létrehozott rövid kódok.", "short_codes_description": "Short codes which were created by you.",
"sidebar_on_left": "Oldalsáv a bal oldalon", "sidebar_on_left": "Oldalsáv a bal oldalon",
"sync": "Szinkronizálás", "sync": "Szinkronizálás",
"sync_collections": "Gyűjtemények", "sync_collections": "Gyűjtemények",
@@ -505,16 +503,16 @@
"theme_description": "Az alkalmazás témájának személyre szabása.", "theme_description": "Az alkalmazás témájának személyre szabása.",
"use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével", "use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével",
"user": "Felhasználó", "user": "Felhasználó",
"verified_email": "Ellenőrzött e-mail-cím", "verified_email": "Verified email",
"verify_email": "E-mail-cím ellenőrzése" "verify_email": "E-mail-cím ellenőrzése"
}, },
"shortcodes": { "shortcodes": {
"actions": "Műveletek", "actions": "Actions",
"created_on": "Létrehozva", "created_on": "Created on",
"deleted": "Rövid kód törölve", "deleted": "Shortcode deleted",
"method": "Módszer", "method": "Method",
"not_found": "A rövid kód nem található", "not_found": "Shortcode not found",
"short_code": "Rövid kód", "short_code": "Short code",
"url": "URL" "url": "URL"
}, },
"shortcut": { "shortcut": {
@@ -556,9 +554,9 @@
"title": "Kérés" "title": "Kérés"
}, },
"response": { "response": {
"copy": "Válasz másolása a vágólapra", "copy": "Copy response to clipboard",
"download": "Válasz letöltés fájlként", "download": "Download response as file",
"title": "Válasz" "title": "Response"
}, },
"theme": { "theme": {
"black": "Téma átváltása fekete módra", "black": "Téma átváltása fekete módra",
@@ -576,8 +574,8 @@
}, },
"socketio": { "socketio": {
"communication": "Kommunikáció", "communication": "Kommunikáció",
"connection_not_authorized": "Ez a SocketIO-kapcsolat nem használ semmilyen hitelesítést.", "connection_not_authorized": "This SocketIO connection does not use any authentication.",
"event_name": "Esemény vagy téma neve", "event_name": "Esemény neve",
"events": "Események", "events": "Események",
"log": "Napló", "log": "Napló",
"url": "URL" "url": "URL"
@@ -594,9 +592,9 @@
"connected": "Kapcsolódva", "connected": "Kapcsolódva",
"connected_to": "Kapcsolódva ehhez: {name}", "connected_to": "Kapcsolódva ehhez: {name}",
"connecting_to": "Kapcsolódás ehhez: {name}…", "connecting_to": "Kapcsolódás ehhez: {name}…",
"connection_error": "Nem sikerült kapcsolódni", "connection_error": "Failed to connect",
"connection_failed": "A kapcsolódás sikertelen", "connection_failed": "Connection failed",
"connection_lost": "A kapcsolat elveszett", "connection_lost": "Connection lost",
"copied_to_clipboard": "Vágólapra másolva", "copied_to_clipboard": "Vágólapra másolva",
"deleted": "Törölve", "deleted": "Törölve",
"deprecated": "ELAVULT", "deprecated": "ELAVULT",
@@ -611,17 +609,17 @@
"history_deleted": "Előzmények törölve", "history_deleted": "Előzmények törölve",
"linewrap": "Sorok tördelése", "linewrap": "Sorok tördelése",
"loading": "Betöltés…", "loading": "Betöltés…",
"message_received": "Üzenet: {message} érkezett ehhez a témához: {topic}", "message_received": "Message: {message} arrived on topic: {topic}",
"mqtt_subscription_failed": "Valami elromlott a következő témára való feliratkozás során: {topic}", "mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
"none": "Nincs", "none": "Nincs",
"nothing_found": "Semmi sem található ehhez:", "nothing_found": "Semmi sem található ehhez:",
"published_error": "Valami elromlott a következő üzenet közzététele során: {topic}, ehhez a témához: {message}", "published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
"published_message": "Közzétett üzenet: {message}, ehhez a témához: {topic}", "published_message": "Published message: {message} to topic: {topic}",
"reconnection_error": "Nem sikerült újrakapcsolódni", "reconnection_error": "Failed to reconnect",
"subscribed_failed": "Nem sikerült feliratkozni erre a témára: {topic}", "subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Sikeresen feliratkozott erre a témára: {topic}", "subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Nem sikerült leiratkozni erről a témáról: {topic}", "unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
"unsubscribed_success": "Sikeresen leiratkozott erről a témáról: {topic}", "unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
"waiting_send_request": "Várakozás a kérés elküldésére" "waiting_send_request": "Várakozás a kérés elküldésére"
}, },
"support": { "support": {
@@ -641,7 +639,7 @@
"body": "Törzs", "body": "Törzs",
"collections": "Gyűjtemények", "collections": "Gyűjtemények",
"documentation": "Dokumentáció", "documentation": "Dokumentáció",
"environments": "Környezetek", "environments": "Environments",
"headers": "Fejlécek", "headers": "Fejlécek",
"history": "Előzmények", "history": "Előzmények",
"mqtt": "MQTT", "mqtt": "MQTT",
@@ -666,7 +664,7 @@
"email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.", "email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"exit": "Kilépés a csapatból", "exit": "Kilépés a csapatból",
"exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból", "exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból",
"invalid_coll_id": "Érvénytelen gyűjteményazonosító", "invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Az e-mail formátuma érvénytelen", "invalid_email_format": "Az e-mail formátuma érvénytelen",
"invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.", "invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"invalid_invite_link": "Érvénytelen meghívási hivatkozás", "invalid_invite_link": "Érvénytelen meghívási hivatkozás",
@@ -690,7 +688,7 @@
"member_removed": "Felhasználó eltávolítva", "member_removed": "Felhasználó eltávolítva",
"member_role_updated": "Felhasználói szerepek frissítve", "member_role_updated": "Felhasználói szerepek frissítve",
"members": "Tagok", "members": "Tagok",
"more_members": "+{count} további", "more_members": "+{count} more",
"name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie", "name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie",
"name_updated": "Csapatnév frissítve", "name_updated": "Csapatnév frissítve",
"new": "Új csapat", "new": "Új csapat",
@@ -698,13 +696,13 @@
"new_name": "Saját új csapat", "new_name": "Saját új csapat",
"no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez", "no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez",
"no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.", "no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"no_request_found": "A kérés nem található.", "no_request_found": "Request not found.",
"not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.", "not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.", "not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"parent_coll_move": "Nem lehet áthelyezni a gyűjteményt egy gyermekgyűjteménybe", "parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Függőben lévő meghívások", "pending_invites": "Függőben lévő meghívások",
"permissions": "Jogosultságok", "permissions": "Jogosultságok",
"same_target_destination": "Ugyanaz a cél és célhely", "same_target_destination": "Same target and destination",
"saved": "Csapat elmentve", "saved": "Csapat elmentve",
"select_a_team": "Csapat kiválasztása", "select_a_team": "Csapat kiválasztása",
"title": "Csapatok", "title": "Csapatok",
@@ -712,9 +710,9 @@
"we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz." "we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz."
}, },
"team_environment": { "team_environment": {
"deleted": "Környezet törölve", "deleted": "Environment Deleted",
"duplicate": "Környezet megkettőzve", "duplicate": "Environment Duplicated",
"not_found": "A környezet nem található." "not_found": "Environment not found."
}, },
"test": { "test": {
"failed": "teszt sikertelen", "failed": "teszt sikertelen",
@@ -734,9 +732,9 @@
"url": "URL" "url": "URL"
}, },
"workspace": { "workspace": {
"change": "Munkaterület váltása", "change": "Change workspace",
"personal": "Saját munkaterület", "personal": "My Workspace",
"team": "Csapat-munkaterület", "team": "Team Workspace",
"title": "Munkaterületek" "title": "Workspaces"
} }
} }

View File

@@ -432,7 +432,6 @@
"view_my_links": "Lihat tautan saya" "view_my_links": "Lihat tautan saya"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Response Body", "body": "Response Body",
"filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)", "filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)",
"headers": "Headers", "headers": "Headers",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Waktu", "time": "Waktu",
"title": "Response", "title": "Response",
"video": "Video",
"waiting_for_connection": "Menunggu koneksi", "waiting_for_connection": "Menunggu koneksi",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corpo della risposta", "body": "Corpo della risposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Intestazioni", "headers": "Intestazioni",
@@ -446,7 +445,6 @@
"status": "Stato", "status": "Stato",
"time": "Tempo impiegato", "time": "Tempo impiegato",
"title": "Risposta", "title": "Risposta",
"video": "Video",
"waiting_for_connection": "In attesa di connessione", "waiting_for_connection": "In attesa di connessione",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "自分のリンクを見る" "view_my_links": "自分のリンクを見る"
}, },
"response": { "response": {
"audio": "Audio",
"body": "レスポンスボディ", "body": "レスポンスボディ",
"filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)", "filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)",
"headers": "ヘッダー", "headers": "ヘッダー",
@@ -446,7 +445,6 @@
"status": "ステータス", "status": "ステータス",
"time": "時間", "time": "時間",
"title": "レスポンス", "title": "レスポンス",
"video": "Video",
"waiting_for_connection": "接続を待っています", "waiting_for_connection": "接続を待っています",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "내 링크 보기" "view_my_links": "내 링크 보기"
}, },
"response": { "response": {
"audio": "Audio",
"body": "응답 본문", "body": "응답 본문",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "헤더", "headers": "헤더",
@@ -446,7 +445,6 @@
"status": "상태", "status": "상태",
"time": "시간", "time": "시간",
"title": "제목", "title": "제목",
"video": "Video",
"waiting_for_connection": "연결 대기 중", "waiting_for_connection": "연결 대기 중",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Reactie inhoud", "body": "Reactie inhoud",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers", "headers": "Headers",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Tijd", "time": "Tijd",
"title": "Antwoord", "title": "Antwoord",
"video": "Video",
"waiting_for_connection": "wachten op verbinding", "waiting_for_connection": "wachten op verbinding",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Svarkropp", "body": "Svarkropp",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Overskrifter", "headers": "Overskrifter",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Tid", "time": "Tid",
"title": "Respons", "title": "Respons",
"video": "Video",
"waiting_for_connection": "venter på tilkobling", "waiting_for_connection": "venter på tilkobling",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Ciało odpowiedzi", "body": "Ciało odpowiedzi",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Nagłówki", "headers": "Nagłówki",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Czas", "time": "Czas",
"title": "Odpowiedź", "title": "Odpowiedź",
"video": "Video",
"waiting_for_connection": "oczekiwanie na połączenie", "waiting_for_connection": "oczekiwanie na połączenie",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corpo de Resposta", "body": "Corpo de Resposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Cabeçalhos", "headers": "Cabeçalhos",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Tempo", "time": "Tempo",
"title": "Resposta", "title": "Resposta",
"video": "Video",
"waiting_for_connection": "aguardando conexão", "waiting_for_connection": "aguardando conexão",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corpo de Resposta", "body": "Corpo de Resposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Cabeçalhos", "headers": "Cabeçalhos",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Tempo", "time": "Tempo",
"title": "Resposta", "title": "Resposta",
"video": "Video",
"waiting_for_connection": "aguardando conexão", "waiting_for_connection": "aguardando conexão",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "Vizualizare link-uri" "view_my_links": "Vizualizare link-uri"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corpul de răspuns", "body": "Corpul de răspuns",
"filter_response_body": "Filtrează corpul răspunsului JSON (folosește sintaxa JSONPath)", "filter_response_body": "Filtrează corpul răspunsului JSON (folosește sintaxa JSONPath)",
"headers": "Anteturi", "headers": "Anteturi",
@@ -446,7 +445,6 @@
"status": "Stare", "status": "Stare",
"time": "Timp", "time": "Timp",
"title": "Raspuns", "title": "Raspuns",
"video": "Video",
"waiting_for_connection": "Așteptând conexiunea", "waiting_for_connection": "Așteptând conexiunea",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Тело ответа", "body": "Тело ответа",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Заголовки", "headers": "Заголовки",
@@ -446,7 +445,6 @@
"status": "Статус", "status": "Статус",
"time": "Время", "time": "Время",
"title": "Ответ", "title": "Ответ",
"video": "Video",
"waiting_for_connection": "Ожидание соединения", "waiting_for_connection": "Ожидание соединения",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Тело за одговор", "body": "Тело за одговор",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Заглавља", "headers": "Заглавља",
@@ -446,7 +445,6 @@
"status": "Статус", "status": "Статус",
"time": "време", "time": "време",
"title": "Одговор", "title": "Одговор",
"video": "Video",
"waiting_for_connection": "чека везу", "waiting_for_connection": "чека везу",
"xml": "КСМЛ" "xml": "КСМЛ"
}, },

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Svarskommitté", "body": "Svarskommitté",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Rubriker", "headers": "Rubriker",
@@ -446,7 +445,6 @@
"status": "Status", "status": "Status",
"time": "Tid", "time": "Tid",
"title": "Svar", "title": "Svar",
"video": "Video",
"waiting_for_connection": "väntar på anslutning", "waiting_for_connection": "väntar på anslutning",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -122,7 +122,7 @@
"edit": "Koleksiyonu düzenle", "edit": "Koleksiyonu düzenle",
"invalid_name": "Lütfen koleksiyon için geçerli bir ad girin", "invalid_name": "Lütfen koleksiyon için geçerli bir ad girin",
"invalid_root_move": "Collection already in the root", "invalid_root_move": "Collection already in the root",
"moved": "Başarıyla taşındı", "moved": "Moved Successfully",
"my_collections": "Koleksiyonlarım", "my_collections": "Koleksiyonlarım",
"name": "Yeni Koleksiyonum", "name": "Yeni Koleksiyonum",
"name_length_insufficient": "Koleksiyon adı en az 3 karakter uzunluğunda olmalıdır", "name_length_insufficient": "Koleksiyon adı en az 3 karakter uzunluğunda olmalıdır",
@@ -147,7 +147,7 @@
"remove_team": "Bu takımı silmek istediğinizden emin misiniz?", "remove_team": "Bu takımı silmek istediğinizden emin misiniz?",
"remove_telemetry": "Telemetriden çıkmak istediğinizden emin misiniz?", "remove_telemetry": "Telemetriden çıkmak istediğinizden emin misiniz?",
"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": "Bu sekmede yapılan değişiklikleri kaydetmek istiyor musunuz?", "save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Bu çalışma alanını senkronize etmek istediğinizden emin misiniz?" "sync": "Bu çalışma alanını senkronize etmek istediğinizden emin misiniz?"
}, },
"count": { "count": {
@@ -368,9 +368,9 @@
}, },
"profile": { "profile": {
"app_settings": "Uygulama ayarları", "app_settings": "Uygulama ayarları",
"default_hopp_displayname": "Adsız Kullanıcı", "default_hopp_displayname": "Unnamed User",
"editor": "Editör", "editor": "Düzenleyici",
"editor_description": "Editörler istekleri ekleyebilir, düzenleyebilir ve silebilir.", "editor_description": "Editors can add, edit, and delete requests.",
"email_verification_mail": "Doğrulama bağlantısı e-postanıza gönderildi. E-postanızı doğrulamak için gelen bağlantıya tıklayınız.", "email_verification_mail": "Doğrulama bağlantısı e-postanıza gönderildi. E-postanızı doğrulamak için gelen bağlantıya tıklayınız.",
"no_permission": "Bu eylemi gerçekleştirmek için gerekli yetkiniz yok.", "no_permission": "Bu eylemi gerçekleştirmek için gerekli yetkiniz yok.",
"owner": "Kurucu", "owner": "Kurucu",
@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Yanıt gövdesi", "body": "Yanıt gövdesi",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Başlıklar", "headers": "Başlıklar",
@@ -446,7 +445,6 @@
"status": "Durum", "status": "Durum",
"time": "Zaman", "time": "Zaman",
"title": "Cevap", "title": "Cevap",
"video": "Video",
"waiting_for_connection": "Bağlantı için bekleniyor", "waiting_for_connection": "Bağlantı için bekleniyor",
"xml": "XML" "xml": "XML"
}, },

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,6 @@
"view_my_links": "檢視我的連結" "view_my_links": "檢視我的連結"
}, },
"response": { "response": {
"audio": "音訊",
"body": "回應本體", "body": "回應本體",
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)", "filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
"headers": "回應標頭", "headers": "回應標頭",
@@ -446,7 +445,6 @@
"status": "狀態", "status": "狀態",
"time": "時間", "time": "時間",
"title": "回應", "title": "回應",
"video": "視訊",
"waiting_for_connection": "等待連線", "waiting_for_connection": "等待連線",
"xml": "XML" "xml": "XML"
}, },
@@ -494,7 +492,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 +549,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 +568,7 @@
}, },
"show": { "show": {
"code": "顯示程式碼", "code": "顯示程式碼",
"collection": "顯示合面板", "collection": "顯示合面板",
"more": "顯示更多", "more": "顯示更多",
"sidebar": "顯示側邊欄" "sidebar": "顯示側邊欄"
}, },
@@ -639,9 +637,9 @@
"tab": { "tab": {
"authorization": "授權", "authorization": "授權",
"body": "請求本體", "body": "請求本體",
"collections": "合", "collections": "合",
"documentation": "幫助文件", "documentation": "幫助文件",
"environments": "環境", "environments": "Environments",
"headers": "請求標頭", "headers": "請求標頭",
"history": "歷史記錄", "history": "歷史記錄",
"mqtt": "MQTT", "mqtt": "MQTT",
@@ -666,7 +664,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 +688,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 +732,9 @@
"url": "網址" "url": "網址"
}, },
"workspace": { "workspace": {
"change": "切換工作區", "change": "Change workspace",
"personal": "我的工作區", "personal": "My Workspace",
"team": "團隊工作區", "team": "Team Workspace",
"title": "工作區" "title": "Workspaces"
} }
} }

View File

@@ -432,7 +432,6 @@
"view_my_links": "Переглянути мої посилання" "view_my_links": "Переглянути мої посилання"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Орган реагування", "body": "Орган реагування",
"filter_response_body": "Фільтр тіла відповідей JSON (використовує синтаксис JSONPath)", "filter_response_body": "Фільтр тіла відповідей JSON (використовує синтаксис JSONPath)",
"headers": "Заголовки", "headers": "Заголовки",
@@ -446,7 +445,6 @@
"status": "Статус", "status": "Статус",
"time": "Час", "time": "Час",
"title": "Відповідь", "title": "Відповідь",
"video": "Video",
"waiting_for_connection": "очікування підключення", "waiting_for_connection": "очікування підключення",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -4,7 +4,7 @@
"cancel": "Hủy bỏ", "cancel": "Hủy bỏ",
"choose_file": "Chọn một tệp", "choose_file": "Chọn một tệp",
"clear": "Thông thoáng", "clear": "Thông thoáng",
"clear_all": "Quet sạch tt cả", "clear_all": "Quet sạch tât cả",
"close": "Close", "close": "Close",
"connect": "Liên kết", "connect": "Liên kết",
"connecting": "Connecting", "connecting": "Connecting",
@@ -432,7 +432,6 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Cơ quan phản hồi", "body": "Cơ quan phản hồi",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Tiêu đề", "headers": "Tiêu đề",
@@ -446,7 +445,6 @@
"status": "Tình trạng", "status": "Tình trạng",
"time": "Thời gian", "time": "Thời gian",
"title": "Phản ứng", "title": "Phản ứng",
"video": "Video",
"waiting_for_connection": "Đang đợi kết nối", "waiting_for_connection": "Đang đợi kết nối",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@hoppscotch/common", "name": "@hoppscotch/common",
"private": true, "private": true,
"version": "2023.4.7", "version": "2023.4.3",
"scripts": { "scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",
"dev:vite": "vite", "dev:vite": "vite",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -82,7 +82,6 @@ declare module '@vue/runtime-core' {
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']
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']
@@ -93,7 +92,6 @@ declare module '@vue/runtime-core' {
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']
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']
@@ -123,7 +121,6 @@ declare module '@vue/runtime-core' {
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default'] HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['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']
IconLucideBrush: typeof import('~icons/lucide/brush')['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']
@@ -133,19 +130,15 @@ declare module '@vue/runtime-core' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default'] IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['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']
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']
LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default'] LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default'] LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default'] LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default'] LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default'] LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default'] LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default'] LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default'] ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']

View File

@@ -44,9 +44,8 @@
class="flex flex-col items-center justify-center p-4 text-secondaryLight" class="flex flex-col items-center justify-center p-4 text-secondaryLight"
> >
<icon-lucide-search class="pb-2 opacity-75 svg-icons" /> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
<span class="my-2 text-center flex flex-col"> <span class="my-2 text-center">
{{ t("state.nothing_found") }} {{ t("state.nothing_found") }} "{{ filterText }}"
<span class="break-all">"{{ filterText }}"</span>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -284,14 +284,6 @@ const importerAction = async (stepResults: StepReturnValue[]) => {
emit("import-to-teams", result) emit("import-to-teams", result)
} else { } else {
appendRESTCollections(result) appendRESTCollections(result)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
importer: importerModule.value!.name,
platform: "rest",
workspaceType: "personal",
})
fileImported() fileImported()
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1 bg-primary">
<div <div
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight" class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
:style=" :style="

View File

@@ -89,7 +89,6 @@ import {
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 { currentActiveTab } from "~/helpers/rest/tab"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -224,13 +223,6 @@ const saveRequestAs = async () => {
}, },
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "personal",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "my-folder") { } else if (picked.value.pickedType === "my-folder") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
@@ -251,13 +243,6 @@ const saveRequestAs = async () => {
}, },
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "personal",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "my-request") { } else if (picked.value.pickedType === "my-request") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
@@ -279,38 +264,17 @@ const saveRequestAs = async () => {
}, },
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "rest",
workspaceType: "personal",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "teams-collection") { } else if (picked.value.pickedType === "teams-collection") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request") throw new Error("requestUpdated is not a REST Request")
updateTeamCollectionOrFolder(picked.value.collectionID, requestUpdated) updateTeamCollectionOrFolder(picked.value.collectionID, requestUpdated)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "team",
})
} else if (picked.value.pickedType === "teams-folder") { } else if (picked.value.pickedType === "teams-folder") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request") throw new Error("requestUpdated is not a REST Request")
updateTeamCollectionOrFolder(picked.value.folderID, requestUpdated) updateTeamCollectionOrFolder(picked.value.folderID, requestUpdated)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "team",
})
} else if (picked.value.pickedType === "teams-request") { } else if (picked.value.pickedType === "teams-request") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request") throw new Error("requestUpdated is not a REST Request")
@@ -328,13 +292,6 @@ const saveRequestAs = async () => {
title: requestUpdated.name, title: requestUpdated.name,
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "rest",
workspaceType: "team",
})
pipe( pipe(
updateTeamRequest(picked.value.requestID, data), updateTeamRequest(picked.value.requestID, data),
TE.match( TE.match(
@@ -356,13 +313,6 @@ const saveRequestAs = async () => {
requestUpdated as HoppGQLRequest requestUpdated as HoppGQLRequest
) )
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "gql",
workspaceType: "team",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "gql-my-folder") { } else if (picked.value.pickedType === "gql-my-folder") {
// TODO: Check for GQL request ? // TODO: Check for GQL request ?
@@ -371,13 +321,6 @@ const saveRequestAs = async () => {
requestUpdated as HoppGQLRequest requestUpdated as HoppGQLRequest
) )
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "gql",
workspaceType: "team",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "gql-my-collection") { } else if (picked.value.pickedType === "gql-my-collection") {
// TODO: Check for GQL request ? // TODO: Check for GQL request ?
@@ -386,13 +329,6 @@ const saveRequestAs = async () => {
requestUpdated as HoppGQLRequest requestUpdated as HoppGQLRequest
) )
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "gql",
workspaceType: "team",
})
requestSaved() requestSaved()
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1 bg-primary">
<div <div
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight" class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
:style=" :style="

View File

@@ -46,7 +46,6 @@ import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data" import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
import { addGraphqlCollection } from "~/newstore/collections" import { addGraphqlCollection } from "~/newstore/collections"
import { platform } from "~/platform"
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -80,13 +79,6 @@ export default defineComponent({
) )
this.hideModal() this.hideModal()
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: true,
platform: "gql",
workspaceType: "personal",
})
}, },
hideModal() { hideModal() {
this.name = null this.name = null

View File

@@ -244,14 +244,6 @@ const importFromJSON = () => {
return return
} }
appendGraphqlCollections(collections) appendGraphqlCollections(collections)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
importer: "json",
workspaceType: "personal",
platform: "gql",
})
fileImported() fileImported()
} }
reader.readAsText(inputChooseFileToImportFrom.value.files[0]) reader.readAsText(inputChooseFileToImportFrom.value.files[0])
@@ -265,12 +257,6 @@ const exportJSON = () => {
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url a.href = url
platform?.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "gql",
})
// TODO: get uri from meta // TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json` a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a) document.body.appendChild(a)

View File

@@ -153,7 +153,6 @@ import IconArchive from "~icons/lucide/archive"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { platform } from "~/platform"
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -286,13 +285,6 @@ export default defineComponent({
response: "", response: "",
}) })
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
platform: "gql",
createdNow: true,
workspaceType: "personal",
})
this.displayModalAddRequest(false) this.displayModalAddRequest(false)
}, },
addRequest(payload) { addRequest(payload) {
@@ -302,14 +294,6 @@ export default defineComponent({
}, },
onAddFolder({ name, path }) { onAddFolder({ name, path }) {
addGraphqlFolder(name, path) addGraphqlFolder(name, path)
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: false,
platform: "gql",
workspaceType: "personal",
})
this.displayModalAddFolder(false) this.displayModalAddFolder(false)
}, },
addFolder(payload) { addFolder(payload) {

View File

@@ -125,8 +125,8 @@
@hide-modal="displayModalEditFolder(false)" @hide-modal="displayModalEditFolder(false)"
/> />
<CollectionsEditRequest <CollectionsEditRequest
v-model="editingRequestName"
:show="showModalEditRequest" :show="showModalEditRequest"
v-bind:model-value="editingRequest ? editingRequest.name : ''"
:loading-state="modalLoadingState" :loading-state="modalLoadingState"
@submit="updateEditingRequest" @submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)" @hide-modal="displayModalEditRequest(false)"
@@ -157,7 +157,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, PropType, ref, watch } from "vue" import { computed, PropType, ref, watch } from "vue"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
@@ -288,7 +288,6 @@ const editingFolder = ref<
const editingFolderName = ref<string | null>(null) const editingFolderName = ref<string | null>(null)
const editingFolderPath = ref<string | null>(null) const editingFolderPath = ref<string | null>(null)
const editingRequest = ref<HoppRESTRequest | null>(null) const editingRequest = ref<HoppRESTRequest | null>(null)
const editingRequestName = ref("")
const editingRequestIndex = ref<number | null>(null) const editingRequestIndex = ref<number | null>(null)
const editingRequestID = ref<string | null>(null) const editingRequestID = ref<string | null>(null)
@@ -599,25 +598,11 @@ const addNewRootCollection = (name: string) => {
}) })
) )
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
platform: "rest",
workspaceType: "personal",
isRootCollection: true,
})
displayModalAdd(false) displayModalAdd(false)
} else if (hasTeamWriteAccess.value) { } else if (hasTeamWriteAccess.value) {
if (!collectionsType.value.selectedTeam) return if (!collectionsType.value.selectedTeam) return
modalLoadingState.value = true modalLoadingState.value = true
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
platform: "rest",
workspaceType: "team",
isRootCollection: true,
})
pipe( pipe(
createNewRootCollection(name, collectionsType.value.selectedTeam.id), createNewRootCollection(name, collectionsType.value.selectedTeam.id),
TE.match( TE.match(
@@ -666,13 +651,6 @@ const onAddRequest = (requestName: string) => {
}, },
}) })
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
workspaceType: "personal",
createdNow: true,
platform: "rest",
})
displayModalAddRequest(false) displayModalAddRequest(false)
} else if (hasTeamWriteAccess.value) { } else if (hasTeamWriteAccess.value) {
const folder = editingFolder.value const folder = editingFolder.value
@@ -688,13 +666,6 @@ const onAddRequest = (requestName: string) => {
title: requestName, title: requestName,
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
workspaceType: "team",
platform: "rest",
createdNow: true,
})
pipe( pipe(
createRequestInCollection(folder.id, data), createRequestInCollection(folder.id, data),
TE.match( TE.match(
@@ -740,14 +711,6 @@ const onAddFolder = (folderName: string) => {
if (collectionsType.value.type === "my-collections") { if (collectionsType.value.type === "my-collections") {
if (!path) return if (!path) return
addRESTFolder(folderName, path) addRESTFolder(folderName, path)
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
workspaceType: "personal",
isRootCollection: false,
platform: "rest",
})
displayModalAddFolder(false) displayModalAddFolder(false)
} else if (hasTeamWriteAccess.value) { } else if (hasTeamWriteAccess.value) {
const folder = editingFolder.value const folder = editingFolder.value
@@ -755,13 +718,6 @@ const onAddFolder = (folderName: string) => {
modalLoadingState.value = true modalLoadingState.value = true
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
workspaceType: "personal",
isRootCollection: false,
platform: "rest",
})
pipe( pipe(
createChildCollection(folderName, folder.id), createChildCollection(folderName, folder.id),
TE.match( TE.match(
@@ -904,7 +860,6 @@ const editRequest = (payload: {
}) => { }) => {
const { folderPath, requestIndex, request } = payload const { folderPath, requestIndex, request } = payload
editingRequest.value = request editingRequest.value = request
editingRequestName.value = request.name ?? ""
if (collectionsType.value.type === "my-collections" && folderPath) { if (collectionsType.value.type === "my-collections" && folderPath) {
editingFolderPath.value = folderPath editingFolderPath.value = folderPath
editingRequestIndex.value = parseInt(requestIndex) editingRequestIndex.value = parseInt(requestIndex)
@@ -938,9 +893,6 @@ const updateEditingRequest = (newName: string) => {
if (possibleActiveTab) { if (possibleActiveTab) {
possibleActiveTab.value.document.request.name = requestUpdated.name possibleActiveTab.value.document.request.name = requestUpdated.name
nextTick(() => {
possibleActiveTab.value.document.isDirty = false
})
} }
displayModalEditRequest(false) displayModalEditRequest(false)
@@ -979,9 +931,6 @@ const updateEditingRequest = (newName: string) => {
if (possibleTab) { if (possibleTab) {
possibleTab.value.document.request.name = requestName possibleTab.value.document.request.name = requestName
nextTick(() => {
possibleTab.value.document.isDirty = false
})
} }
} }
} }
@@ -1927,12 +1876,6 @@ const exportData = async (
} }
const exportJSONCollection = async () => { const exportJSONCollection = async () => {
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
await getJSONCollection() await getJSONCollection()
initializeDownloadCollection(collectionJSON.value, null) initializeDownloadCollection(collectionJSON.value, null)
@@ -1944,12 +1887,6 @@ const createCollectionGist = async () => {
return return
} }
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "gist",
platform: "rest",
})
creatingGistCollection.value = true creatingGistCollection.value = true
await getJSONCollection() await getJSONCollection()
@@ -1980,12 +1917,6 @@ const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
importingMyCollections.value = true importingMyCollections.value = true
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "import-to-teams",
platform: "rest",
})
pipe( pipe(
importJSONToTeam( importJSONToTeam(
JSON.stringify(collection), JSON.stringify(collection),

View File

@@ -190,12 +190,6 @@ const createEnvironmentGist = async () => {
) )
toast.success(t("export.gist_created").toString()) toast.success(t("export.gist_created").toString())
platform.analytics?.logEvent({
type: "HOPP_EXPORT_ENVIRONMENT",
platform: "rest",
})
window.open(res.data.html_url) window.open(res.data.html_url)
} catch (e) { } catch (e) {
toast.error(t("error.something_went_wrong").toString()) toast.error(t("error.something_went_wrong").toString())
@@ -255,13 +249,6 @@ const openDialogChooseFileToImportFrom = () => {
const importToTeams = async (content: Environment[]) => { const importToTeams = async (content: Environment[]) => {
loading.value = true loading.value = true
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: "team",
})
for (const [i, env] of content.entries()) { for (const [i, env] of content.entries()) {
if (i === content.length - 1) { if (i === content.length - 1) {
await pipe( await pipe(
@@ -314,12 +301,6 @@ const importFromJSON = () => {
return return
} }
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: "personal",
})
const reader = new FileReader() const reader = new FileReader()
reader.onload = ({ target }) => { reader.onload = ({ target }) => {
@@ -371,7 +352,6 @@ const importFromPostman = ({
const environment: Environment = { name, variables: [] } const environment: Environment = { name, variables: [] }
values.forEach(({ key, value }) => environment.variables.push({ key, value })) values.forEach(({ key, value }) => environment.variables.push({ key, value }))
const environments = [environment] const environments = [environment]
importFromHoppscotch(environments) importFromHoppscotch(environments)
} }

View File

@@ -11,14 +11,13 @@
class="bg-transparent border-b border-dividerLight select-wrapper" class="bg-transparent border-b border-dividerLight select-wrapper"
> >
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconLayers" v-if="selectedEnv.type !== 'NO_ENV_SELECTED'"
:label=" :label="selectedEnv.name"
mdAndLarger class="flex-1 !justify-start pr-8 rounded-none"
? selectedEnv.type !== 'NO_ENV_SELECTED' />
? selectedEnv.name <HoppButtonSecondary
: `${t('environment.select')}` v-else
: '' :label="`${t('environment.select')}`"
"
class="flex-1 !justify-start pr-8 rounded-none" class="flex-1 !justify-start pr-8 rounded-none"
/> />
</span> </span>
@@ -47,19 +46,11 @@
} }
" "
/> />
<HoppSmartTabs <div v-if="environmentType === 'my-environments'" class="flex flex-col">
v-model="selectedEnvTab" <hr v-if="myEnvironments.length > 0" />
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary"
render-inactive-tabs
>
<HoppSmartTab
:id="'my-environments'"
:label="`${t('environment.my_environments')}`"
>
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in myEnvironments" v-for="(gen, index) in myEnvironments"
:key="`gen-${index}`" :key="`gen-${index}`"
:icon="IconLayers"
:label="gen.name" :label="gen.name"
:info-icon="index === selectedEnv.index ? IconCheck : undefined" :info-icon="index === selectedEnv.index ? IconCheck : undefined"
:active-info-icon="index === selectedEnv.index" :active-info-icon="index === selectedEnv.index"
@@ -70,38 +61,20 @@
} }
" "
/> />
<div
v-if="myEnvironments.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>
</HoppSmartTab> <div v-else class="flex flex-col">
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
>
<div <div
v-if="teamListLoading" v-if="teamEnvLoading"
class="flex flex-col items-center justify-center p-4" class="flex flex-col items-center justify-center p-4"
> >
<HoppSmartSpinner class="my-4" /> <HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span> <span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div> </div>
<div v-else-if="isTeamSelected" class="flex flex-col"> <hr v-if="teamEnvironmentList.length > 0" />
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in teamEnvironmentList" v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`" :key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name" :label="gen.environment.name"
:info-icon=" :info-icon="
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
@@ -119,77 +92,47 @@
} }
" "
/> />
<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>
<div <div
v-if="!teamListLoading && teamAdapterError" v-if="!teamEnvLoading && isAdapterError"
class="flex flex-col items-center py-4" class="flex flex-col items-center py-4"
> >
<icon-lucide-help-circle class="mb-4 svg-icons" /> <icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }} {{ errorMessage }}
</div>
</div> </div>
</HoppSmartTab>
</HoppSmartTabs>
</div> </div>
</template> </template>
</tippy> </tippy>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from "vue" import { computed, ref } from "vue"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { useReadonlyStream, useStream } from "~/composables/stream" import { Environment } from "@hoppscotch/data"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { useStream } from "~/composables/stream"
import { import {
environments$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
const t = useI18n() const t = useI18n()
const colorMode = useColorMode()
type EnvironmentType = "my-environments" | "team-environments" type EnvironmentType = "my-environments" | "team-environments"
const myEnvironments = useReadonlyStream(environments$, []) const props = defineProps<{
environmentType: EnvironmentType
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" }) myEnvironments: Environment[]
teamEnvironmentList: TeamEnvironment[]
const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined) teamEnvLoading: boolean
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false) isAdapterError: boolean
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null) errorMessage: GQLError<string>
const teamEnvironmentList = useReadonlyStream( isTeamSelected: boolean
teamEnvListAdapter.teamEnvironmentList$, }>()
[]
)
const selectedEnvironmentIndex = useStream( const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
@@ -197,67 +140,15 @@ const selectedEnvironmentIndex = useStream(
setSelectedEnvironmentIndex 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)
}
}
}
)
// TeamList-Adapter
const teamListAdapter = new TeamListAdapter(true)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
REMEMBERED_TEAM_ID.value = team.id
changeWorkspace({
teamID: team.id,
teamName: team.name,
type: "team",
})
}
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) switchToTeamWorkspace(team)
}
}
}
)
const selectedEnv = computed(() => { const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") { if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return { return {
type: "MY_ENV", type: "MY_ENV",
index: selectedEnvironmentIndex.value.index, index: selectedEnvironmentIndex.value.index,
name: myEnvironments.value[selectedEnvironmentIndex.value.index].name, name: props.myEnvironments[selectedEnvironmentIndex.value.index].name,
} }
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") { } else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find( const teamEnv = props.teamEnvironmentList.find(
(env) => (env) =>
env.id === env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" && (selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
@@ -279,17 +170,4 @@ const selectedEnv = computed(() => {
// Template refs // Template refs
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -4,6 +4,15 @@
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"
> >
<WorkspaceCurrent :section="t('tab.environments')" /> <WorkspaceCurrent :section="t('tab.environments')" />
<EnvironmentsSelector
:environment-type="environmentType.type"
:my-environments="myEnvironments"
:team-env-loading="loading"
:team-environment-list="teamEnvironmentList"
:is-adapter-error="adapterError !== null"
:error-message="adapterError ? getErrorMessage(adapterError) : ''"
:is-team-selected="environmentType.selectedTeam !== undefined"
/>
<EnvironmentsMyEnvironment <EnvironmentsMyEnvironment
environment-index="Global" environment-index="Global"
:environment="globalEnvironment" :environment="globalEnvironment"
@@ -37,11 +46,13 @@ 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 {
environments$,
globalEnv$, globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { GQLError } from "~/helpers/backend/GQLClient"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { workspaceStatus$ } from "~/newstore/workspace" import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter" import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
@@ -136,19 +147,24 @@ onLoggedIn(() => {
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" }) const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// Switch to my environments if workspace is personal and to team environments if workspace is team // Used to switch environment type and team when user switch workspace in the global workspace switcher
// also resets selected environment if workspace is personal and the previous selected environment was a team environment // Check if there is a teamID in the workspace, if yes, switch to team environment and select the team
// If there is no teamID, switch to my environment
watch(workspace, (newWorkspace) => { watch(workspace, (newWorkspace) => {
if (newWorkspace.type === "personal") { if (newWorkspace.type === "personal") {
switchToMyEnvironments() switchToMyEnvironments()
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
if (selectedEnvironmentIndex.value.type !== "MY_ENV") { if (selectedEnvironmentIndex.value.type !== "MY_ENV") {
setSelectedEnvironmentIndex({ setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED", type: "NO_ENV_SELECTED",
}) })
} }
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
} }
}) })
@@ -191,6 +207,8 @@ defineActionHandler(
} }
) )
const myEnvironments = useReadonlyStream(environments$, [])
const selectedEnvironmentIndex = useStream( const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" }, { type: "NO_ENV_SELECTED" },
@@ -233,4 +251,17 @@ watch(
}, },
{ deep: true } { deep: true }
) )
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -148,7 +148,6 @@ import { useToast } from "@composables/toast"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { environmentsStore } from "~/newstore/environments" import { environmentsStore } from "~/newstore/environments"
import { platform } from "~/platform"
type EnvironmentVariable = { type EnvironmentVariable = {
id: number id: number
@@ -166,8 +165,8 @@ const props = withDefaults(
defineProps<{ defineProps<{
show: boolean show: boolean
action: "edit" | "new" action: "edit" | "new"
editingEnvironmentIndex?: number | "Global" | null editingEnvironmentIndex: number | "Global" | null
editingVariableName?: string | null editingVariableName: string | null
envVars?: () => Environment["variables"] envVars?: () => Environment["variables"]
}>(), }>(),
{ {
@@ -312,11 +311,6 @@ const saveEnvironment = () => {
index: envList.value.length - 1, index: envList.value.length - 1,
}) })
toast.success(`${t("environment.created")}`) toast.success(`${t("environment.created")}`)
platform.analytics?.logEvent({
type: "HOPP_CREATE_ENVIRONMENT",
workspaceType: "personal",
})
} else if (props.editingEnvironmentIndex === "Global") { } else if (props.editingEnvironmentIndex === "Global") {
// Editing the Global environment // Editing the Global environment
setGlobalEnvVariables(environmentUpdated.variables) setGlobalEnvVariables(environmentUpdated.variables)

View File

@@ -140,7 +140,7 @@ import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { flow, pipe } from "fp-ts/function" import { flow, pipe } from "fp-ts/function"
import { Environment, parseTemplateStringE } from "@hoppscotch/data" import { parseTemplateStringE } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { clone } from "lodash-es" import { clone } from "lodash-es"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
@@ -156,7 +156,6 @@ import IconTrash from "~icons/lucide/trash"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconDone from "~icons/lucide/check" import IconDone from "~icons/lucide/check"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import { platform } from "~/platform"
type EnvironmentVariable = { type EnvironmentVariable = {
id: number id: number
@@ -174,20 +173,16 @@ const props = withDefaults(
defineProps<{ defineProps<{
show: boolean show: boolean
action: "edit" | "new" action: "edit" | "new"
editingEnvironment?: TeamEnvironment | null editingEnvironment: TeamEnvironment | null
editingTeamId: string | undefined editingTeamId: string | undefined
editingVariableName?: string | null editingVariableName: string | null
isViewer?: boolean isViewer: boolean
envVars?: () => Environment["variables"]
}>(), }>(),
{ {
show: false, show: false,
action: "edit", action: "edit",
editingEnvironment: null, editingEnvironment: null,
editingTeamId: "", editingTeamId: "",
editingVariableName: null,
isViewer: false,
envVars: () => [],
} }
) )
@@ -231,16 +226,10 @@ watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
if (props.action === "new") { if (props.editingEnvironment === null) {
name.value = null name.value = null
vars.value = pipe( vars.value = []
props.envVars() ?? [], } else {
A.map((e: { key: string; value: string }) => ({
id: idTicker.value++,
env: clone(e),
}))
)
} else if (props.editingEnvironment !== null) {
name.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 ?? [],
@@ -295,11 +284,6 @@ const saveEnvironment = async () => {
) )
if (props.action === "new") { if (props.action === "new") {
platform.analytics?.logEvent({
type: "HOPP_CREATE_ENVIRONMENT",
workspaceType: "team",
})
await pipe( await pipe(
createTeamEnvironment( createTeamEnvironment(
JSON.stringify(filterdVariables), JSON.stringify(filterdVariables),

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div <div
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary" class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperSecondaryStickyFold border-dividerLight bg-primary"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="team === undefined || team.myRole === 'VIEWER'" v-if="team === undefined || team.myRole === 'VIEWER'"

View File

@@ -143,51 +143,33 @@
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="basicUsername" v-model="basicUsername"
: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="basicPassword" v-model="basicPassword"
:environment-highlights="false"
:placeholder="t('authorization.password')" :placeholder="t('authorization.password')"
/> />
</div> </div>
</div> </div>
<div v-if="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="bearerToken" placeholder="Token" />
v-model="bearerToken"
:environment-highlights="false"
placeholder="Token"
/>
</div> </div>
</div> </div>
<div v-if="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="oauth2Token" placeholder="Token" />
v-model="oauth2Token"
:environment-highlights="false"
placeholder="Token"
/>
</div> </div>
<HttpOAuth2Authorization /> <HttpOAuth2Authorization />
</div> </div>
<div v-if="authType === 'api-key'"> <div v-if="authType === 'api-key'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput v-model="apiKey" placeholder="Key" />
v-model="apiKey"
:environment-highlights="false"
placeholder="Key"
/>
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput v-model="apiValue" placeholder="Value" />
v-model="apiValue"
:environment-highlights="false"
placeholder="Value"
/>
</div> </div>
<div class="flex items-center border-b border-dividerLight"> <div class="flex items-center border-b border-dividerLight">
<span class="flex items-center"> <span class="flex items-center">

View File

@@ -17,7 +17,6 @@
<HoppButtonPrimary <HoppButtonPrimary
id="get" id="get"
name="get" name="get"
: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"
@@ -32,12 +31,7 @@ import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network" import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "@composables/stream" import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
gqlAuth$,
gqlHeaders$,
gqlURL$,
setGQLURL,
} from "~/newstore/GQLSession"
const t = useI18n() const t = useI18n()
@@ -46,21 +40,15 @@ const props = defineProps<{
}>() }>()
const connected = useReadonlyStream(props.conn.connected$, false) const connected = useReadonlyStream(props.conn.connected$, false)
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
const headers = useReadonlyStream(gqlHeaders$, []) const headers = useReadonlyStream(gqlHeaders$, [])
const auth = useReadonlyStream(gqlAuth$, {
authType: "none",
authActive: true,
})
const url = useStream(gqlURL$, "", setGQLURL) const url = useStream(gqlURL$, "", setGQLURL)
const onConnectClick = () => { const onConnectClick = () => {
if (!connected.value) { if (!connected.value) {
props.conn.connect(url.value, headers.value as any, auth.value) props.conn.connect(url.value, headers.value as any)
platform.analytics?.logEvent({ platform.analytics?.logHoppRequestRunToAnalytics({
type: "HOPP_REQUEST_RUN",
platform: "graphql-schema", platform: "graphql-schema",
strategy: getCurrentStrategyID(), strategy: getCurrentStrategyID(),
}) })

View File

@@ -748,8 +748,7 @@ const runQuery = async () => {
console.error(e) console.error(e)
} }
platform.analytics?.logEvent({ platform.analytics?.logHoppRequestRunToAnalytics({
type: "HOPP_REQUEST_RUN",
platform: "graphql-query", platform: "graphql-query",
strategy: getCurrentStrategyID(), strategy: getCurrentStrategyID(),
}) })

View File

@@ -63,7 +63,7 @@ import { GQLHistoryEntry } from "~/newstore/history"
import { shortDateTime } from "~/helpers/utils/date" import { shortDateTime } from "~/helpers/utils/date"
import IconStar from "~icons/lucide/star" import IconStar from "~icons/lucide/star"
import IconStarOff from "~icons/hopp/star-off" import IconStarOff from "~icons/lucide/star-off"
import IconTrash from "~icons/lucide/trash" import IconTrash from "~icons/lucide/trash"
import IconMinimize2 from "~icons/lucide/minimize-2" import IconMinimize2 from "~icons/lucide/minimize-2"
import IconMaximize2 from "~icons/lucide/maximize-2" import IconMaximize2 from "~icons/lucide/maximize-2"

View File

@@ -72,11 +72,9 @@
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group" class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
> >
<span <span
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary truncate" class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary"
> >
<icon-lucide-chevron-right <icon-lucide-chevron-right class="mr-2 indicator" />
class="mr-2 indicator flex flex-shrink-0"
/>
<span <span
:class="[ :class="[
{ 'capitalize-first': groupSelection === 'TIME' }, { 'capitalize-first': groupSelection === 'TIME' },

View File

@@ -55,7 +55,7 @@ import { RESTHistoryEntry } from "~/newstore/history"
import { shortDateTime } from "~/helpers/utils/date" import { shortDateTime } from "~/helpers/utils/date"
import IconStar from "~icons/lucide/star" import IconStar from "~icons/lucide/star"
import IconStarOff from "~icons/hopp/star-off" import IconStarOff from "~icons/lucide/star-off"
import IconTrash from "~icons/lucide/trash" import IconTrash from "~icons/lucide/trash"
const props = defineProps<{ const props = defineProps<{

View File

@@ -165,7 +165,6 @@ import IconCheck from "~icons/lucide/check"
import IconWrapText from "~icons/lucide/wrap-text" import IconWrapText from "~icons/lucide/wrap-text"
import { currentActiveTab } from "~/helpers/rest/tab" import { currentActiveTab } from "~/helpers/rest/tab"
import cloneDeep from "lodash-es/cloneDeep" import cloneDeep from "lodash-es/cloneDeep"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
@@ -249,10 +248,6 @@ watch(
(goingToShow) => { (goingToShow) => {
if (goingToShow) { if (goingToShow) {
request.value = cloneDeep(currentActiveTab.value.document.request) request.value = cloneDeep(currentActiveTab.value.document.request)
platform.analytics?.logEvent({
type: "HOPP_REST_CODEGEN_OPENED",
})
} }
} }
) )

View File

@@ -338,7 +338,7 @@ watch(workingHeaders, (headersList) => {
// Sync logic between headers and working/bulk headers // Sync logic between headers and working/bulk headers
watch( watch(
() => request.value.headers, request.value.headers,
(newHeadersList) => { (newHeadersList) => {
// Sync should overwrite working headers // Sync should overwrite working headers
const filteredWorkingHeaders = pipe( const filteredWorkingHeaders = pipe(

View File

@@ -94,7 +94,6 @@ import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import { currentActiveTab } from "~/helpers/rest/tab" import { currentActiveTab } from "~/helpers/rest/tab"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
@@ -145,10 +144,6 @@ const handleImport = () => {
try { try {
const req = parseCurlToHoppRESTReq(text) const req = parseCurlToHoppRESTReq(text)
platform.analytics?.logEvent({
type: "HOPP_REST_IMPORT_CURL",
})
currentActiveTab.value.document.request = req currentActiveTab.value.document.request = req
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View File

@@ -217,7 +217,6 @@
@hide-modal="showCodegenModal = false" @hide-modal="showCodegenModal = false"
/> />
<CollectionsSaveRequest <CollectionsSaveRequest
v-if="showSaveRequestModal"
mode="rest" mode="rest"
:show="showSaveRequestModal" :show="showSaveRequestModal"
@hide-modal="showSaveRequestModal = false" @hide-modal="showSaveRequestModal = false"
@@ -324,8 +323,7 @@ const newSendRequest = async () => {
loading.value = true loading.value = true
// Log the request run into analytics // Log the request run into analytics
platform.analytics?.logEvent({ platform.analytics?.logHoppRequestRunToAnalytics({
type: "HOPP_REQUEST_RUN",
platform: "rest", platform: "rest",
strategy: getCurrentStrategyID(), strategy: getCurrentStrategyID(),
}) })
@@ -447,11 +445,6 @@ const copyRequest = async () => {
shareLink.value = "" shareLink.value = ""
fetchingShareLink.value = true fetchingShareLink.value = true
const shortcodeResult = await createShortcode(tab.value.document.request)() const shortcodeResult = await createShortcode(tab.value.document.request)()
platform.analytics?.logEvent({
type: "HOPP_SHORTCODE_CREATED",
})
if (E.isLeft(shortcodeResult)) { if (E.isLeft(shortcodeResult)) {
toast.error(`${shortcodeResult.left.error}`) toast.error(`${shortcodeResult.left.error}`)
shareLink.value = `${t("error.something_went_wrong")}` shareLink.value = `${t("error.something_went_wrong")}`
@@ -522,14 +515,6 @@ const saveRequest = () => {
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req) editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
tab.value.document.isDirty = false tab.value.document.isDirty = false
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
platform: "rest",
createdNow: false,
workspaceType: "personal",
})
toast.success(`${t("request.saved")}`) toast.success(`${t("request.saved")}`)
} catch (e) { } catch (e) {
tab.value.document.saveContext = undefined tab.value.document.saveContext = undefined
@@ -540,13 +525,6 @@ const saveRequest = () => {
// TODO: handle error case (NOTE: overwriteRequestTeams is async) // TODO: handle error case (NOTE: overwriteRequestTeams is async)
try { try {
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
platform: "rest",
createdNow: false,
workspaceType: "team",
})
runMutation(UpdateRequestDocument, { runMutation(UpdateRequestDocument, {
requestID: saveCtx.requestID, requestID: saveCtx.requestID,
data: { data: {

View File

@@ -54,8 +54,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { HoppRESTRequest } from "@hoppscotch/data" import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core" import { computed, ref, watch } from "vue"
import { computed, ref } from "vue"
export type RequestOptionTabs = export type RequestOptionTabs =
| "params" | "params"
@@ -71,7 +70,15 @@ const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTRequest): void (e: "update:modelValue", value: HoppRESTRequest): void
}>() }>()
const request = useVModel(props, "modelValue", emit) const request = ref(props.modelValue)
watch(
() => request.value,
(newVal) => {
emit("update:modelValue", newVal)
},
{ deep: true }
)
const selectedRealtimeTab = ref<RequestOptionTabs>("params") const selectedRealtimeTab = ref<RequestOptionTabs>("params")

View File

@@ -28,11 +28,9 @@
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group" class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
> >
<span <span
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary truncate" class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary"
> >
<icon-lucide-chevron-right <icon-lucide-chevron-right class="mr-2 indicator" />
class="mr-2 indicator flex flex-shrink-0"
/>
<span class="truncate capitalize-first"> <span class="truncate capitalize-first">
{{ t("environment.title") }} {{ t("environment.title") }}
</span> </span>
@@ -199,20 +197,11 @@
/> />
</div> </div>
<EnvironmentsMyDetails <EnvironmentsMyDetails
:show="showMyEnvironmentDetailsModal" :show="showModalDetails"
action="new" action="new"
:env-vars="getAdditionVars" :env-vars="getAdditionVars"
@hide-modal="displayModalAdd(false)" @hide-modal="displayModalAdd(false)"
/> />
<EnvironmentsTeamsDetails
:show="showTeamEnvironmentDetailsModal"
action="new"
:env-vars="getAdditionVars"
:editing-team-id="
workspace.type === 'team' ? workspace.teamID : undefined
"
@hide-modal="displayModalAdd(false)"
/>
</div> </div>
</template> </template>
@@ -236,7 +225,6 @@ import IconClose from "~icons/lucide/x"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { workspaceStatus$ } from "~/newstore/workspace"
const props = defineProps<{ const props = defineProps<{
modelValue: HoppTestResult | null | undefined modelValue: HoppTestResult | null | undefined
@@ -251,15 +239,10 @@ const testResults = useVModel(props, "modelValue", emit)
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" }) const showModalDetails = ref(false)
const showMyEnvironmentDetailsModal = ref(false)
const showTeamEnvironmentDetailsModal = ref(false)
const displayModalAdd = (shouldDisplay: boolean) => { const displayModalAdd = (shouldDisplay: boolean) => {
if (workspace.value.type === "personal") showModalDetails.value = shouldDisplay
showMyEnvironmentDetailsModal.value = shouldDisplay
else showTeamEnvironmentDetailsModal.value = shouldDisplay
} }
/** /**

View File

@@ -44,7 +44,6 @@ const props = withDefaults(
envs?: { key: string; value: string; source: string }[] | null envs?: { key: string; value: string; source: string }[] | null
focus?: boolean focus?: boolean
selectTextOnMount?: boolean selectTextOnMount?: boolean
environmentHighlights?: boolean
readonly?: boolean readonly?: boolean
}>(), }>(),
{ {
@@ -54,7 +53,6 @@ const props = withDefaults(
envs: null, envs: null,
focus: false, focus: false,
readonly: false, readonly: false,
environmentHighlights: true,
} }
) )
@@ -144,7 +142,7 @@ const initView = (el: any) => {
tooltips({ tooltips({
position: "absolute", position: "absolute",
}), }),
props.environmentHighlights ? envTooltipPlugin : [], envTooltipPlugin,
placeholderExt(props.placeholder), placeholderExt(props.placeholder),
EditorView.domEventHandlers({ EditorView.domEventHandlers({
paste(ev) { paste(ev) {

View File

@@ -44,7 +44,6 @@ import { createTeam } from "~/helpers/backend/mutations/Team"
import { TeamNameCodec } from "~/helpers/backend/types/TeamName" import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
@@ -69,12 +68,6 @@ const addNewTeam = async () => {
TE.fromEither, TE.fromEither,
TE.mapLeft(() => "invalid_name" as const), TE.mapLeft(() => "invalid_name" as const),
TE.chainW(createTeam), TE.chainW(createTeam),
TE.chainFirstIOK(
() => () =>
platform.analytics?.logEvent({
type: "HOPP_CREATE_TEAM",
})
),
TE.match( TE.match(
(err) => { (err) => {
// err is of type "invalid_name" | GQLError<Err> // err is of type "invalid_name" | GQLError<Err>

View File

@@ -99,7 +99,7 @@ export class GQLConnection {
private timeoutSubscription: any private timeoutSubscription: any
public connect(url: string, headers: GQLHeader[], auth: HoppGQLAuth) { public connect(url: string, headers: GQLHeader[]) {
if (this.connected$.value) { if (this.connected$.value) {
throw new Error( throw new Error(
"A connection is already running. Close it before starting another." "A connection is already running. Close it before starting another."
@@ -110,7 +110,7 @@ export class GQLConnection {
this.connected$.next(true) this.connected$.next(true)
const poll = async () => { const poll = async () => {
await this.getSchema(url, headers, auth) await this.getSchema(url, headers)
this.timeoutSubscription = setTimeout(() => { this.timeoutSubscription = setTimeout(() => {
poll() poll()
}, GQL_SCHEMA_POLL_INTERVAL) }, GQL_SCHEMA_POLL_INTERVAL)
@@ -135,11 +135,7 @@ export class GQLConnection {
this.schema$.next(null) this.schema$.next(null)
} }
private async getSchema( private async getSchema(url: string, headers: GQLHeader[]) {
url: string,
reqHeaders: GQLHeader[],
auth: HoppGQLAuth
) {
try { try {
this.isLoading$.next(true) this.isLoading$.next(true)
@@ -147,38 +143,10 @@ export class GQLConnection {
query: getIntrospectionQuery(), query: getIntrospectionQuery(),
}) })
const headers = reqHeaders.filter((x) => x.active && x.key !== "")
// TODO: Support a better b64 implementation than btoa ?
if (auth.authType === "basic") {
const username = auth.username
const password = auth.password
headers.push({
active: true,
key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`,
})
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
headers.push({
active: true,
key: "Authorization",
value: `Bearer ${auth.token}`,
})
} else if (auth.authType === "api-key") {
const { key, value, addTo } = auth
if (addTo === "Headers") {
headers.push({
active: true,
key,
value,
})
}
}
const finalHeaders: Record<string, string> = {} const finalHeaders: Record<string, string> = {}
headers.forEach((x) => (finalHeaders[x.key] = x.value)) headers
.filter((x) => x.active && x.key !== "")
.forEach((x) => (finalHeaders[x.key] = x.value))
const reqOptions = { const reqOptions = {
method: "POST", method: "POST",

View File

@@ -6,18 +6,14 @@ import { isJSONContentType } from "./utils/contenttypes"
* Handles translations for all the hopp.io REST Shareable URL params * Handles translations for all the hopp.io REST Shareable URL params
*/ */
export function translateExtURLParams( export function translateExtURLParams(
urlParams: Record<string, any>, urlParams: Record<string, any>
initialReq?: HoppRESTRequest
): HoppRESTRequest { ): HoppRESTRequest {
if (urlParams.v) return parseV1ExtURL(urlParams, initialReq) if (urlParams.v) return parseV1ExtURL(urlParams)
else return parseV0ExtURL(urlParams, initialReq) else return parseV0ExtURL(urlParams)
} }
function parseV0ExtURL( function parseV0ExtURL(urlParams: Record<string, any>): HoppRESTRequest {
urlParams: Record<string, any>, const resolvedReq = getDefaultRESTRequest()
initialReq?: HoppRESTRequest
): HoppRESTRequest {
const resolvedReq = initialReq ?? getDefaultRESTRequest()
if (urlParams.method && typeof urlParams.method === "string") { if (urlParams.method && typeof urlParams.method === "string") {
resolvedReq.method = urlParams.method resolvedReq.method = urlParams.method
@@ -93,11 +89,8 @@ function parseV0ExtURL(
return resolvedReq return resolvedReq
} }
function parseV1ExtURL( function parseV1ExtURL(urlParams: Record<string, any>): HoppRESTRequest {
urlParams: Record<string, any>, const resolvedReq = getDefaultRESTRequest()
initialReq?: HoppRESTRequest
): HoppRESTRequest {
const resolvedReq = initialReq ?? getDefaultRESTRequest()
if (urlParams.headers && typeof urlParams.headers === "string") { if (urlParams.headers && typeof urlParams.headers === "string") {
resolvedReq.headers = JSON.parse(urlParams.headers) resolvedReq.headers = JSON.parse(urlParams.headers)

View File

@@ -71,11 +71,9 @@ const parseURL = (urlText: string | number) =>
* @returns URL object * @returns URL object
*/ */
export function getURLObject(parsedArguments: parser.Arguments) { export function getURLObject(parsedArguments: parser.Arguments) {
const location = parsedArguments.location ?? undefined
return pipe( return pipe(
// contains raw url strings // contains raw url strings
[...parsedArguments._.slice(1), location], parsedArguments._.slice(1),
A.findFirstMap(parseURL), A.findFirstMap(parseURL),
// no url found // no url found
O.getOrElse(() => new URL(defaultRESTReq.endpoint)) O.getOrElse(() => new URL(defaultRESTReq.endpoint))

View File

@@ -14,13 +14,7 @@ let keybindingsEnabled = true
* Alt is also regarded as macOS OPTION (⌥) key * Alt is also regarded as macOS OPTION (⌥) key
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!) * Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
*/ */
type ModifierKeys = type ModifierKeys = "ctrl" | "alt" | "ctrl-shift" | "alt-shift"
| "ctrl"
| "alt"
| "ctrl-shift"
| "alt-shift"
| "ctrl-alt"
| "ctrl-alt-shift"
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
// prettier-ignore // prettier-ignore
@@ -149,19 +143,18 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
} }
function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null { function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
const modifierKeys = { const isShiftKey = ev.shiftKey
ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
alt: ev.altKey,
shift: ev.shiftKey,
}
// active modifier: ctrl | alt | ctrl-alt | ctrl-shift | ctrl-alt-shift | alt-shift // We only allow one modifier key to be pressed (for now)
// modiferKeys object's keys are sorted to match the above order // Control key (+ Command) gets priority and if Alt is also pressed, it is ignored
const activeModifier = Object.keys(modifierKeys) if (isAppleDevice() && ev.metaKey) return isShiftKey ? "ctrl-shift" : "ctrl"
.filter((key) => modifierKeys[key as keyof typeof modifierKeys]) else if (!isAppleDevice() && ev.ctrlKey)
.join("-") return isShiftKey ? "ctrl-shift" : "ctrl"
return activeModifier === "" ? null : (activeModifier as ModifierKeys) // Test for Alt key
if (ev.altKey) return isShiftKey ? "alt-shift" : "alt"
return null
} }
/** /**

View File

@@ -105,8 +105,7 @@ export class MQTTConnection {
this.handleError(e) this.handleError(e)
} }
platform.analytics?.logEvent({ platform.analytics?.logHoppRequestRunToAnalytics({
type: "HOPP_REQUEST_RUN",
platform: "mqtt", platform: "mqtt",
}) })
} }

View File

@@ -113,8 +113,7 @@ export class SIOConnection {
this.handleError(error, "CONNECTION") this.handleError(error, "CONNECTION")
} }
platform.analytics?.logEvent({ platform.analytics?.logHoppRequestRunToAnalytics({
type: "HOPP_REQUEST_RUN",
platform: "socketio", platform: "socketio",
}) })
} }

View File

@@ -63,8 +63,7 @@ export class SSEConnection {
}) })
} }
platform.analytics?.logEvent({ platform.analytics?.logHoppRequestRunToAnalytics({
type: "HOPP_REQUEST_RUN",
platform: "sse", platform: "sse",
}) })
} }

View File

@@ -71,8 +71,7 @@ export class WSConnection {
this.handleError(error as SyntaxError) this.handleError(error as SyntaxError)
} }
platform.analytics?.logEvent({ platform.analytics?.logHoppRequestRunToAnalytics({
type: "HOPP_REQUEST_RUN",
platform: "wss", platform: "wss",
}) })
} }

View File

@@ -6,7 +6,6 @@ import { refWithControl } from "@vueuse/core"
import { HoppRESTResponse } from "../types/HoppRESTResponse" import { HoppRESTResponse } from "../types/HoppRESTResponse"
import { getDefaultRESTRequest } from "./default" import { getDefaultRESTRequest } from "./default"
import { HoppTestResult } from "../types/HoppTestResult" import { HoppTestResult } from "../types/HoppTestResult"
import { platform } from "~/platform"
export type HoppRESTTab = { export type HoppRESTTab = {
id: string id: string
@@ -148,10 +147,6 @@ export function createNewTab(document: HoppRESTDocument, switchToIt = true) {
currentTabID.value = id currentTabID.value = id
} }
platform.analytics?.logEvent({
type: "HOPP_REST_NEW_TAB_OPENED",
})
return tab return tab
} }

View File

@@ -275,4 +275,7 @@ export const gqlResponse$ = gqlSessionStore.subject$.pipe(
distinctUntilChanged() distinctUntilChanged()
) )
export const gqlAuth$ = gqlSessionStore.subject$.pipe(pluck("request", "auth")) export const gqlAuth$ = gqlSessionStore.subject$.pipe(
pluck("request", "auth"),
distinctUntilChanged()
)

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